From bd84f09a96f3106aaa0e32f0c5101b39ff06c779 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 22 Jun 2017 17:02:30 +0300 Subject: [PATCH] Implement correct path measurement, matrix calculation, getTextAnchorShift, startOffset. Add method and spacing attributes to textPath. Correct startOffset calculation. Implement method="stretch". --- .../com/horcrux/svg/BezierTransformer.java | 186 ------------------ .../java/com/horcrux/svg/TSpanShadowNode.java | 107 +++++----- .../com/horcrux/svg/TextPathShadowNode.java | 30 ++- .../java/com/horcrux/svg/TextShadowNode.java | 49 ++--- elements/TextPath.js | 2 + lib/attributes.js | 2 + lib/extract/extractText.js | 4 + 7 files changed, 109 insertions(+), 271 deletions(-) delete mode 100644 android/src/main/java/com/horcrux/svg/BezierTransformer.java diff --git a/android/src/main/java/com/horcrux/svg/BezierTransformer.java b/android/src/main/java/com/horcrux/svg/BezierTransformer.java deleted file mode 100644 index 8aeb0a44..00000000 --- a/android/src/main/java/com/horcrux/svg/BezierTransformer.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (c) 2015-present, Horcrux. - * All rights reserved. - * - * This source code is licensed under the MIT-style license found in the - * LICENSE file in the root directory of this source tree. - */ - - -package com.horcrux.svg; - -import android.graphics.Matrix; -import android.graphics.PointF; - -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; - -public class BezierTransformer { - private ReadableArray mBezierCurves; - private PathShadowNode mPath; - private int mCurrentBezierIndex = 0; - private float mStartOffset = 0f; - private float mLastOffset = 0f; - private float mLastRecord = 0f; - private float mLastDistance = 0f; - private PointF mLastPoint = new PointF(); - private PointF mP0 = new PointF(); - private PointF mP1 = new PointF(); - private PointF mP2 = new PointF(); - private PointF mP3 = new PointF(); - private boolean mReachedStart; - private boolean mReachedEnd; - - BezierTransformer(PathShadowNode path, float startOffset) { - mBezierCurves = path.getBezierCurves(); - mStartOffset = startOffset; - mPath = path; - } - - private float calculateBezier(float t, float P0, float P1, float P2, float P3) { - return (1-t)*(1-t)*(1-t)*P0+3*(1-t)*(1-t)*t*P1+3*(1-t)*t*t*P2+t*t*t*P3; - } - - private PointF pointAtOffset(float t) { - float x = calculateBezier(t, mP0.x, mP1.x, mP2.x, mP3.x); - float y = calculateBezier(t, mP0.y, mP1.y, mP2.y, mP3.y); - return new PointF(x, y); - } - - private float calculateBezierPrime(float t, float P0, float P1, float P2, float P3) { - return -3*(1-t)*(1-t)*P0+(3*(1-t)*(1-t)*P1)-(6*t*(1-t)*P1)-(3*t*t*P2)+(6*t*(1-t)*P2)+3*t*t*P3; - } - - private float angleAtOffset(float t) { - float dx = calculateBezierPrime(t, mP0.x, mP1.x, mP2.x, mP3.x); - float dy = calculateBezierPrime(t, mP0.y, mP1.y, mP2.y, mP3.y); - return (float)Math.atan2(dy, dx); - } - - private float calculateDistance(PointF a, PointF b) { - return (float)Math.hypot(a.x - b.x, a.y - b.y); - } - - private PointF getPointFromMap(ReadableMap map) { - return new PointF((float)map.getDouble("x"), (float)map.getDouble("y")); - } - - // Simplistic routine to find the offset along Bezier that is - // `distance` away from `point`. `offset` is the offset used to - // generate `point`, and saves us the trouble of recalculating it - // This routine just walks forward until it finds a point at least - // `distance` away. Good optimizations here would reduce the number - // of guesses, but this is tricky since if we go too far out, the - // curve might loop back on leading to incorrect results. Tuning - // kStep is good start. - private float offsetAtDistance(float distance, PointF point, float offset) { - float kStep = 0.001f; // 0.0001 - 0.001 work well - float newDistance = 0; - float newOffset = offset + kStep; - while (newDistance <= distance && newOffset < 1.0) { - newOffset += kStep; - newDistance = calculateDistance(point, pointAtOffset(newOffset)); - } - - mLastDistance = newDistance; - return newOffset; - } - - private void setControlPoints() { - ReadableArray bezier = mBezierCurves.getArray(mCurrentBezierIndex++); - - if (bezier != null) { - // set start point - if (bezier.size() == 1) { - mLastPoint = mP0 = getPointFromMap(bezier.getMap(0)); - setControlPoints(); - } else if (bezier.size() == 3) { - mP1 = getPointFromMap(bezier.getMap(0)); - mP2 = getPointFromMap(bezier.getMap(1)); - mP3 = getPointFromMap(bezier.getMap(2)); - } - } - } - - public float getStartOffset() { - return mStartOffset; - } - - public PathShadowNode getPath() { - return mPath; - } - - public float getTotalDistance() { - float distance = 0; - - while (!mReachedEnd) { - distance += 0.1f; - float offset = offsetAtDistance(distance - mLastRecord, mLastPoint, mLastOffset); - - if (offset < 1) { - PointF glyphPoint = pointAtOffset(offset); - mLastOffset = offset; - mLastPoint = glyphPoint; - mLastRecord = distance; - } else if (mBezierCurves.size() == mCurrentBezierIndex) { - mReachedEnd = true; - } else { - mLastOffset = 0; - mLastPoint = mP0 = mP3; - mLastRecord += mLastDistance; - setControlPoints(); - } - } - - mCurrentBezierIndex = 0; - mLastOffset = 0f; - mLastRecord = 0f; - mLastDistance = 0f; - mLastPoint = new PointF(); - mP0 = new PointF(); - mP1 = new PointF(); - mP2 = new PointF(); - mP3 = new PointF(); - mReachedEnd = false; - - return distance; - } - - public Matrix getTransformAtDistance(float distance) { - mReachedStart = distance >= 0; - - if (mReachedEnd || !mReachedStart) { - return new Matrix(); - } - - float offset = offsetAtDistance(distance - mLastRecord, mLastPoint, mLastOffset); - - if (offset < 1) { - PointF glyphPoint = pointAtOffset(offset); - mLastOffset = offset; - mLastPoint = glyphPoint; - mLastRecord = distance; - Matrix matrix = new Matrix(); - matrix.setRotate((float)Math.toDegrees(angleAtOffset(offset))); - matrix.postTranslate(glyphPoint.x, glyphPoint.y); - return matrix; - } else if (mBezierCurves.size() == mCurrentBezierIndex) { - mReachedEnd = true; - return new Matrix(); - } else { - mLastOffset = 0; - mLastPoint = mP0 = mP3; - mLastRecord += mLastDistance; - setControlPoints(); - return getTransformAtDistance(distance); - } - } - - public boolean hasReachedEnd() { - return mReachedEnd; - } - - public boolean hasReachedStart() { - return mReachedStart; - } -} diff --git a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java index d0079990..afc98d00 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java @@ -14,6 +14,7 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PathMeasure; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.Typeface; @@ -24,14 +25,17 @@ import com.facebook.react.uimanager.annotations.ReactProp; import javax.annotation.Nullable; +import static android.graphics.PathMeasure.POSITION_MATRIX_FLAG; +import static android.graphics.PathMeasure.TANGENT_MATRIX_FLAG; + /** * Shadow node for virtual TSpan view */ public class TSpanShadowNode extends TextShadowNode { - private BezierTransformer mBezierTransformer; private Path mCache; private @Nullable String mContent; + private TextPathShadowNode textPath; private static final String PROP_FONT_FAMILY = "fontFamily"; private static final String PROP_FONT_SIZE = "fontSize"; @@ -83,6 +87,21 @@ public class TSpanShadowNode extends TextShadowNode { return path; } + private float getTextAnchorShift(float width) { + float x = 0; + + switch (getComputedTextAnchor()) { + case TEXT_ANCHOR_MIDDLE: + x = -width / 2; + break; + case TEXT_ANCHOR_END: + x = -width; + break; + } + + return x; + } + private Path getLinePath(Canvas canvas, String line, Paint paint) { ReadableMap font = applyTextPropertiesToPaint(paint); int length = line.length(); @@ -92,29 +111,22 @@ public class TSpanShadowNode extends TextShadowNode { return path; } - PointF glyphPoint = getGlyphPointFromContext(0, 0); - PointF glyphDelta = getGlyphDeltaFromContext(); - float textMeasure = 0; - float distance = 0; float offset = 0; - PathShadowNode p; - Path bezierPath; + float distance = 0; + float renderMethodScaling = 1; + float textMeasure = paint.measureText(line); + float textAnchorShift = getTextAnchorShift(textMeasure); - if (mBezierTransformer != null) { - offset = mBezierTransformer.getStartOffset(); - boolean debug = true; - if (debug) { - distance = mBezierTransformer.getTotalDistance(); - textMeasure = paint.measureText(line); - p = mBezierTransformer.getPath(); - bezierPath = p.getPath(); - canvas.drawTextOnPath( - line, - bezierPath, - offset + glyphPoint.x + glyphDelta.x, - glyphDelta.y, - paint - ); + PathMeasure pm = null; + + if (textPath != null) { + pm = new PathMeasure(textPath.getPath(), false); + distance = pm.getLength(); + offset = PropHelper.fromPercentageToFloat(textPath.getStartOffset(), distance, 0, mScale); + String spacing = textPath.getSpacing(); // spacing = "auto | exact" + String method = textPath.getMethod(); // method = "align | stretch" + if ("stretch".equals(method)) { + renderMethodScaling = distance / textMeasure; } } @@ -122,6 +134,8 @@ public class TSpanShadowNode extends TextShadowNode { float width; Matrix matrix; String current; + PointF glyphPoint; + PointF glyphDelta; String previous = ""; float glyphPosition = 0; char[] chars = line.toCharArray(); @@ -133,8 +147,8 @@ public class TSpanShadowNode extends TextShadowNode { paint.getTextWidths(line, widths); for (int index = 0; index < length; index++) { + width = widths[index] * renderMethodScaling; current = String.valueOf(chars[index]); - width = widths[index]; glyph = new Path(); if (isKerningValueSet) { @@ -149,32 +163,42 @@ public class TSpanShadowNode extends TextShadowNode { previous = current; } - glyphPoint = getGlyphPointFromContext(glyphPosition, width); + glyphPoint = getGlyphPointFromContext(textAnchorShift + glyphPosition, width); + glyphDelta = getGlyphDeltaFromContext(); + glyphPosition += width; + matrix = new Matrix(); - if (mBezierTransformer != null) { + if (textPath != null) { float halfway = width / 2; + float start = offset + glyphPoint.x + glyphDelta.x; + float midpoint = start + halfway; - matrix = mBezierTransformer.getTransformAtDistance( - offset + glyphPoint.x + glyphDelta.x + halfway - ); - - if (textPathHasReachedEnd()) { - break; - } else if (!textPathHasReachedStart()) { + if (midpoint > distance ) { + if (start <= distance) { + // Seems to cut off too early, see e.g. toap3, this shows the last "p" + midpoint = start; + halfway = 0; + } else { + break; + } + } else if (midpoint < 0) { continue; } + pm.getMatrix(midpoint, matrix, POSITION_MATRIX_FLAG | TANGENT_MATRIX_FLAG); + matrix.preTranslate(-halfway, glyphDelta.y); + matrix.preScale(renderMethodScaling, 1); matrix.postTranslate(0, glyphPoint.y); } else { - matrix = new Matrix(); - matrix.setTranslate(glyphPoint.x + glyphDelta.x, glyphPoint.y + glyphDelta.y); + matrix.setTranslate( + glyphPoint.x + glyphDelta.x + textAnchorShift, + glyphPoint.y + glyphDelta.y + ); } paint.getTextPath(current, 0, 1, 0, 0, glyph); - glyphDelta = getGlyphDeltaFromContext(); glyph.transform(matrix); - glyphPosition += width; path.addPath(glyph); } @@ -218,8 +242,7 @@ public class TSpanShadowNode extends TextShadowNode { while (parent != null) { if (parent.getClass() == TextPathShadowNode.class) { - TextPathShadowNode textPath = (TextPathShadowNode)parent; - mBezierTransformer = textPath.getBezierTransformer(); + textPath = (TextPathShadowNode)parent; break; } else if (!(parent instanceof TextShadowNode)) { break; @@ -228,12 +251,4 @@ public class TSpanShadowNode extends TextShadowNode { parent = parent.getParent(); } } - - private boolean textPathHasReachedEnd() { - return mBezierTransformer.hasReachedEnd(); - } - - private boolean textPathHasReachedStart() { - return mBezierTransformer.hasReachedStart(); - } } diff --git a/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java b/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java index fb6221c6..9e07d9b0 100644 --- a/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java @@ -23,6 +23,8 @@ import javax.annotation.Nullable; public class TextPathShadowNode extends TextShadowNode { private String mHref; + private String mMethod; + private String mSpacing; private @Nullable String mStartOffset; @ReactProp(name = "href") @@ -37,12 +39,36 @@ public class TextPathShadowNode extends TextShadowNode { markUpdated(); } + @ReactProp(name = "method") + public void setMethod(@Nullable String method) { + mMethod = method; + markUpdated(); + } + + @ReactProp(name = "spacing") + public void setSpacing(@Nullable String spacing) { + mSpacing = spacing; + markUpdated(); + } + + public String getMethod() { + return mMethod; + } + + public String getSpacing() { + return mSpacing; + } + + public String getStartOffset() { + return mStartOffset; + } + @Override public void draw(Canvas canvas, Paint paint, float opacity) { drawGroup(canvas, paint, opacity); } - public BezierTransformer getBezierTransformer() { + public Path getPath() { SvgViewShadowNode svg = getSvgShadowNode(); VirtualNode template = svg.getDefinedTemplate(mHref); @@ -52,7 +78,7 @@ public class TextPathShadowNode extends TextShadowNode { } PathShadowNode path = (PathShadowNode)template; - return new BezierTransformer(path, relativeOnWidth(mStartOffset)); + return path.getPath(); } @Override diff --git a/android/src/main/java/com/horcrux/svg/TextShadowNode.java b/android/src/main/java/com/horcrux/svg/TextShadowNode.java index 651318f3..b0ad091d 100644 --- a/android/src/main/java/com/horcrux/svg/TextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TextShadowNode.java @@ -33,10 +33,10 @@ import com.facebook.react.uimanager.annotations.ReactProp; public class TextShadowNode extends GroupShadowNode { - private static final int TEXT_ANCHOR_AUTO = 0; - private static final int TEXT_ANCHOR_START = 1; - private static final int TEXT_ANCHOR_MIDDLE = 2; - private static final int TEXT_ANCHOR_END = 3; + static final int TEXT_ANCHOR_AUTO = 0; + static final int TEXT_ANCHOR_START = 1; + static final int TEXT_ANCHOR_MIDDLE = 2; + static final int TEXT_ANCHOR_END = 3; private int mTextAnchor = TEXT_ANCHOR_AUTO; private @Nullable ReadableArray mDeltaX; @@ -86,8 +86,6 @@ public class TextShadowNode extends GroupShadowNode { setupGlyphContext(); clip(canvas, paint); Path path = getGroupPath(canvas, paint); - Matrix matrix = getAlignMatrix(path); - canvas.concat(matrix); drawGroup(canvas, paint, opacity); releaseCachedPath(); } @@ -97,9 +95,6 @@ public class TextShadowNode extends GroupShadowNode { protected Path getPath(Canvas canvas, Paint paint) { setupGlyphContext(); Path groupPath = getGroupPath(canvas, paint); - Matrix matrix = getAlignMatrix(groupPath); - groupPath.transform(matrix); - releaseCachedPath(); return groupPath; } @@ -108,20 +103,21 @@ public class TextShadowNode extends GroupShadowNode { return mTextAnchor; } - private int getComputedTextAnchor() { + int getComputedTextAnchor() { int anchor = mTextAnchor; ReactShadowNode shadowNode = this; - while (shadowNode.getChildCount() > 0 && - anchor == TEXT_ANCHOR_AUTO) { - shadowNode = shadowNode.getChildAt(0); - + while (shadowNode instanceof GroupShadowNode) { if (shadowNode instanceof TextShadowNode) { anchor = ((TextShadowNode) shadowNode).getTextAnchor(); - } else { - break; + if (anchor != TEXT_ANCHOR_AUTO) { + break; + } } + + shadowNode = shadowNode.getParent(); } + return anchor; } @@ -147,25 +143,4 @@ public class TextShadowNode extends GroupShadowNode { protected void pushGlyphContext() { getTextRoot().getGlyphContext().pushContext(mFont, mDeltaX, mDeltaY, mPositionX, mPositionY); } - - private Matrix getAlignMatrix(Path path) { - RectF box = new RectF(); - path.computeBounds(box, true); - - float width = box.width(); - float x = 0; - - switch (getComputedTextAnchor()) { - case TEXT_ANCHOR_MIDDLE: - x = -width / 2; - break; - case TEXT_ANCHOR_END: - x = -width; - break; - } - - Matrix matrix = new Matrix(); - matrix.setTranslate(x, 0); - return matrix; - } } diff --git a/elements/TextPath.js b/elements/TextPath.js index 83774994..236bb461 100644 --- a/elements/TextPath.js +++ b/elements/TextPath.js @@ -16,6 +16,8 @@ export default class extends Shape { ...pathProps, ...fontProps, href: PropTypes.string.isRequired, + method: PropTypes.oneOf(['align', 'stretch']), + spacing: PropTypes.oneOf(['auto', 'exact']), startOffset: numberProp }; diff --git a/lib/attributes.js b/lib/attributes.js index 83c7c31c..68cb285a 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -114,6 +114,8 @@ const TextAttributes = { const TextPathAttributes = { href: true, + method: true, + spacing: true, startOffset: true, ...RenderableAttributes }; diff --git a/lib/extract/extractText.js b/lib/extract/extractText.js index 17fca9d7..e9dea6f2 100644 --- a/lib/extract/extractText.js +++ b/lib/extract/extractText.js @@ -91,6 +91,8 @@ export default function(props, container) { y, dx, dy, + method, + spacing, textAnchor, startOffset } = props; @@ -127,6 +129,8 @@ export default function(props, container) { content, deltaX, deltaY, + method, + spacing, startOffset: (startOffset || 0).toString(), positionX: _.isNil(x) ? null : x.toString(), positionY: _.isNil(y) ? null : y.toString()