mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-21 06:15:15 +00:00
Implement correct path measurement, matrix calculation, getTextAnchorShift, startOffset.
Add method and spacing attributes to textPath. Correct startOffset calculation. Implement method="stretch".
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ const TextAttributes = {
|
||||
|
||||
const TextPathAttributes = {
|
||||
href: true,
|
||||
method: true,
|
||||
spacing: true,
|
||||
startOffset: true,
|
||||
...RenderableAttributes
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user