diff --git a/docs/docs/text/blob.md b/docs/docs/text/blob.md new file mode 100644 index 0000000000..75c2808685 --- /dev/null +++ b/docs/docs/text/blob.md @@ -0,0 +1,38 @@ +--- +id: blob +title: Text Blob +sidebar_label: Text Blob +slug: /text/blob +--- + +A text blob contains glyphs, their positions, and paint attributes specific to text. + +| Name | Type | Description | +|:------------|:-----------|:-------------------------------------------------------------| +| blob | `TextBlob` | Text blob | +| x? | `number` | x coordinate of the origin of the entire run. Default is 0 | +| y? | `number` | y coordinate of the origin of the entire run. Default is 0 | + +## Example + +```tsx twoslash +import {Canvas, TextBlob, Skia} from "@shopify/react-native-skia"; + + +export const HelloWorld = () => { + const typeface = Skia.FontMgr.RefDefault().matchFamilyStyle("helvetica"); + if (!typeface) { + throw new Error("Helvetica not found"); + } + const font = Skia.Font(typeface, 30); + const blob = Skia.TextBlob.MakeFromText("Hello World!", font); + return ( + + + + ); +}; +``` \ No newline at end of file diff --git a/docs/docs/text/path.md b/docs/docs/text/path.md new file mode 100644 index 0000000000..75ddf8e878 --- /dev/null +++ b/docs/docs/text/path.md @@ -0,0 +1,41 @@ +--- +id: path +title: Text Path +sidebar_label: Text Path +slug: /text/path +--- + +Draws text along an SVG path. +The fonts available in the canvas are described in [here](/docs/text/fonts). + +| Name | Type | Description | +|:------------|:-------------------|:-------------------------------------------------------------| +| path | `Path` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.Path.Make()` | +| text | `string` | Text to draw | +| font | `Font` | Font to use (see [Fonts](/docs/text/fonts)) | +| familyName? | `string` | Font family name to use (see [Fonts](/docs/text/fonts)) | +| size? | `number` | Font size if `familName` is provided | + +## Example + +```tsx twoslash +import {Canvas, Group, TextPath, Skia} from "@shopify/react-native-skia"; + +const circle = Skia.Path.Make(); +circle.addCircle(128, 128, 128); + +export const HelloWorld = () => { + return ( + + + + + + ); +}; +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index b88d3d22e3..d0e03b19df 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -47,7 +47,13 @@ const sidebars = { collapsed: true, type: "category", label: "Text", - items: ["text/fonts", "text/text", "text/glyphs"], + items: [ + "text/fonts", + "text/text", + "text/glyphs", + "text/path", + "text/blob", + ], }, { collapsed: true, diff --git a/example/src/Examples/API/Path2.tsx b/example/src/Examples/API/Path2.tsx index 026559b2fc..110f0f627d 100644 --- a/example/src/Examples/API/Path2.tsx +++ b/example/src/Examples/API/Path2.tsx @@ -7,6 +7,8 @@ import { Path, Group, rect, + TextPath, + useFont, } from "@shopify/react-native-skia"; import { Title } from "./components/Title"; @@ -43,6 +45,10 @@ const result = Skia.Path.MakeFromOp(rect1, circle, PathOp.Difference)!; result.simplify(); export const PathExample = () => { + const font = useFont(require("./Roboto-Regular.otf"), 32); + if (!font) { + return null; + } return ( Path Operations @@ -68,6 +74,12 @@ export const PathExample = () => { + Text Path + + + + + ); }; @@ -89,4 +101,8 @@ const styles = StyleSheet.create({ width: SIZE, height: SIZE, }, + textPath: { + width: SIZE, + height: SIZE, + }, }); diff --git a/example/src/Examples/API/Roboto-Regular.otf b/example/src/Examples/API/Roboto-Regular.otf new file mode 100644 index 0000000000..8f0906a79c Binary files /dev/null and b/example/src/Examples/API/Roboto-Regular.otf differ diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h index 53a21ba791..b0d1b2e705 100644 --- a/package/cpp/api/JsiSkApi.h +++ b/package/cpp/api/JsiSkApi.h @@ -35,6 +35,8 @@ #include "JsiSkDataFactory.h" #include "JsiSkFontMgrFactory.h" #include "JsiSkSurfaceFactory.h" +#include "JsiSkTextBlobFactory.h" +#include "JsiSkContourMeasureIter.h" namespace RNSkia { @@ -60,6 +62,7 @@ namespace RNSkia installFunction("XYWHRect", JsiSkRect::createCtor(context)); installFunction("RRectXY", JsiSkRRect::createCtor(context)); installFunction("Point", JsiSkPoint::createCtor(context)); + installFunction("ContourMeasureIter", JsiSkContourMeasureIter::createCtor(context)); installFunction("MakeVertices", JsiSkVertices::createCtor(context)); // Static members @@ -87,6 +90,7 @@ namespace RNSkia "RuntimeEffect", std::make_shared(context)); installReadonlyProperty("Shader", std::make_shared(context)); + installReadonlyProperty("TextBlob", std::make_shared(context)); installReadonlyProperty("Surface", std::make_shared(context)); }; }; diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index 01e27fec63..8ffe51fd51 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -11,6 +11,7 @@ #include "JsiSkRRect.h" #include "JsiSkSVG.h" #include "JsiSkVertices.h" +#include "JsiSkTextBlob.h" #include #include @@ -314,6 +315,15 @@ class JsiSkCanvas : public JsiSkHostObject { return jsi::Value::undefined(); } + JSI_HOST_FUNCTION(drawTextBlob) { + auto blob = JsiSkTextBlob::fromValue(runtime, arguments[0]); + SkScalar x = arguments[1].asNumber(); + SkScalar y = arguments[2].asNumber(); + auto paint = JsiSkPaint::fromValue(runtime, arguments[3]); + _canvas->drawTextBlob(blob, x, y, *paint); + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(drawGlyphs) { auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); auto jsiPositions = arguments[1].asObject(runtime).asArray(runtime); @@ -484,6 +494,7 @@ class JsiSkCanvas : public JsiSkHostObject { JSI_EXPORT_FUNC(JsiSkCanvas, drawPath), JSI_EXPORT_FUNC(JsiSkCanvas, drawVertices), JSI_EXPORT_FUNC(JsiSkCanvas, drawText), + JSI_EXPORT_FUNC(JsiSkCanvas, drawTextBlob), JSI_EXPORT_FUNC(JsiSkCanvas, drawGlyphs), JSI_EXPORT_FUNC(JsiSkCanvas, drawSvg), JSI_EXPORT_FUNC(JsiSkCanvas, clipPath), diff --git a/package/cpp/api/JsiSkContourMeasure.h b/package/cpp/api/JsiSkContourMeasure.h new file mode 100644 index 0000000000..15ad63b2f4 --- /dev/null +++ b/package/cpp/api/JsiSkContourMeasure.h @@ -0,0 +1,94 @@ +#pragma once + + +#include "JsiSkHostObjects.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +#include + +namespace RNSkia { + + using namespace facebook; + + class JsiSkContourMeasure : public JsiSkWrappingSkPtrHostObject { + public: + JsiSkContourMeasure(std::shared_ptr context, + const sk_sp contourMeasure) + : JsiSkWrappingSkPtrHostObject(context, contourMeasure){}; + + JSI_HOST_FUNCTION(getPosTan) { + auto dist = arguments[0].asNumber(); + SkPoint* position = (SkPoint *)malloc(sizeof (SkPoint)); + SkPoint* tangent = (SkPoint *)malloc(sizeof (SkPoint)); + auto result = getObject()->getPosTan(dist, position, tangent); + if (!result) { + jsi::detail::throwJSError(runtime, "getSegment() failed"); + } + auto posTan = jsi::Object(runtime); + posTan.setProperty(runtime, "px", position->x()); + posTan.setProperty(runtime, "py", position->y()); + posTan.setProperty(runtime, "tx", tangent->x()); + posTan.setProperty(runtime, "ty", tangent->y()); + return posTan; + } + + JSI_HOST_FUNCTION(length) { + return jsi::Value(SkScalarToDouble(getObject()->length())); + } + + JSI_HOST_FUNCTION(isClosed) { + return jsi::Value(getObject()->isClosed()); + } + + JSI_HOST_FUNCTION(getSegment) { + auto start = arguments[0].asNumber(); + auto end = arguments[1].asNumber(); + auto startWithMoveTo = arguments[2].getBool(); + SkPath path; + auto result = getObject()->getSegment(start, end, &path, startWithMoveTo); + if (!result) { + jsi::detail::throwJSError(runtime, "getSegment() failed"); + } + return JsiSkPath::toValue(runtime, getContext(), path); + } + + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "ContourMeasure"); + } + + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkContourMeasure, __typename__)) + + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkContourMeasure, getPosTan), + JSI_EXPORT_FUNC(JsiSkContourMeasure, length), + JSI_EXPORT_FUNC(JsiSkContourMeasure, isClosed), + JSI_EXPORT_FUNC(JsiSkContourMeasure, getSegment) + ) + + /** + Returns the underlying object from a host object of this type + */ + static sk_sp fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + return obj.asObject(runtime) + .asHostObject(runtime) + .get() + ->getObject(); + } + + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + sk_sp contourMeasure) { + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(context, contourMeasure) + ); + } + }; +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkContourMeasureIter.h b/package/cpp/api/JsiSkContourMeasureIter.h new file mode 100644 index 0000000000..b652a07e95 --- /dev/null +++ b/package/cpp/api/JsiSkContourMeasureIter.h @@ -0,0 +1,80 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include "JsiSkContourMeasure.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkContourMeasureIter : public JsiSkWrappingSharedPtrHostObject { + public: + JsiSkContourMeasureIter( + std::shared_ptr context, + const SkPath& path, + bool forceClosed, + SkScalar resScale = 1 + ) : JsiSkWrappingSharedPtrHostObject( + context, std::make_shared(path, forceClosed, resScale)) {} + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "ContourMeasureIter"); + } + + JSI_EXPORT_PROPERTY_GETTERS( + JSI_EXPORT_PROP_GET(JsiSkContourMeasureIter, __typename__), + ) + + JSI_HOST_FUNCTION(next) { + return JsiSkContourMeasure::toValue(runtime, getContext(), getObject()->next()); + } + + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkContourMeasureIter, next) + ) + + /** + Returns the underlying object from a host object of this type + */ + static std::shared_ptr fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + return obj.asObject(runtime) + .asHostObject(runtime) + .get() + ->getObject(); + } + + /** + * Creates the function for construction a new instance of the SkContourMeasureIter + * wrapper + * @param context platform context + * @return A function for creating a new host object wrapper for the SkContourMeasureIter + * class + */ + static const jsi::HostFunctionType + createCtor(std::shared_ptr context) { + return JSI_HOST_FUNCTION_LAMBDA { + auto path = JsiSkPath::fromValue(runtime, arguments[0]); + auto forceClosed = arguments[1].getBool(); + auto resScale = arguments[2].asNumber(); + // Return the newly constructed object + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + context, + *path, + forceClosed, + resScale + ) + ); + }; + } + }; +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 8b00ba2dad..2cdcd590f9 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -45,6 +45,28 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return JsiSkRect::toValue(runtime, getContext(), rect); } + JSI_HOST_FUNCTION(getGlyphWidths) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + int bytesPerWidth = 4; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + auto widthPtr = static_cast(malloc(glyphsSize * bytesPerWidth)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + if (count > 1) { + auto paint = JsiSkPaint::fromValue(runtime, arguments[1]); + getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, paint.get()); + } else { + getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, nullptr); + } + auto jsiWidths = jsi::Array(runtime, glyphsSize); + for (int i = 0; i getMetrics(&fm); @@ -53,9 +75,6 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { metrics.setProperty(runtime, "descent", fm.fDescent); metrics.setProperty(runtime, "leading", fm.fLeading); if (!(fm.fFlags & SkFontMetrics::kBoundsInvalid_Flag)) { - const float rect[] = { - fm.fXMin, fm.fTop, fm.fXMax, fm.fBottom - }; auto bounds = SkRect::MakeLTRB(fm.fXMin,fm.fTop, fm.fXMax, fm.fBottom ); auto jsiBounds = JsiSkRect::toValue(runtime, getContext(), bounds); metrics.setProperty(runtime, "bounds", jsiBounds); @@ -92,7 +111,7 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { } std::vector glyphs; - int glyphsSize = static_cast(jsiPositions.size(runtime)); + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); for (int i = 0; i < glyphsSize; i++) { glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); } @@ -207,6 +226,7 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiSkFont, setEmbolden), JSI_EXPORT_FUNC(JsiSkFont, setSubpixel), JSI_EXPORT_FUNC(JsiSkFont, setTypeface), + JSI_EXPORT_FUNC(JsiSkFont, getGlyphWidths) ) JsiSkFont(std::shared_ptr context, const SkFont &font) diff --git a/package/cpp/api/JsiSkPath.h b/package/cpp/api/JsiSkPath.h index 2fccc472fa..8c7b19660b 100644 --- a/package/cpp/api/JsiSkPath.h +++ b/package/cpp/api/JsiSkPath.h @@ -522,6 +522,15 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject { .get() ->getObject(); } + + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + const SkPath &path) { + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(context, path) + ); + } }; } // namespace RNSkia diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h index 2758ae670b..6931d00d38 100644 --- a/package/cpp/api/JsiSkRSXform.h +++ b/package/cpp/api/JsiSkRSXform.h @@ -77,10 +77,10 @@ namespace RNSkia { } /** - * Creates the function for construction a new instance of the SkPoint + * Creates the function for construction a new instance of the SkRSXform * wrapper * @param context platform context - * @return A function for creating a new host object wrapper for the SkPoint + * @return A function for creating a new host object wrapper for the SkRSXform * class */ static const jsi::HostFunctionType diff --git a/package/cpp/api/JsiSkTextBlob.h b/package/cpp/api/JsiSkTextBlob.h new file mode 100644 index 0000000000..9cdefe0bd1 --- /dev/null +++ b/package/cpp/api/JsiSkTextBlob.h @@ -0,0 +1,47 @@ +#pragma once + +#include "JsiSkHostObjects.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkTextBlob : public JsiSkWrappingSkPtrHostObject { + public: + JsiSkTextBlob( + std::shared_ptr context, + sk_sp shader + ) : JsiSkWrappingSkPtrHostObject(context, shader) {} + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "TextBlob"); + } + + JSI_EXPORT_PROPERTY_GETTERS( + JSI_EXPORT_PROP_GET(JsiSkTextBlob, __typename__), + ) + + /** + Returns the underlying object from a host object of this type + */ + static sk_sp fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + const auto object = obj.asObject(runtime); + return object + .asHostObject(runtime) + .get() + ->getObject(); + + } + + }; +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkTextBlobFactory.h b/package/cpp/api/JsiSkTextBlobFactory.h new file mode 100644 index 0000000000..c6d8d44758 --- /dev/null +++ b/package/cpp/api/JsiSkTextBlobFactory.h @@ -0,0 +1,94 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include "JsiSkTextBlob.h" +#include "JsiSkRSXform.h" +#include +#include + +namespace RNSkia { + + using namespace facebook; + + class JsiSkTextBlobFactory : public JsiSkHostObject { + public: + JSI_HOST_FUNCTION(MakeFromText) { + auto str = arguments[0].asString(runtime).utf8(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[1]); + auto textBlob = SkTextBlob::MakeFromString(str.c_str(), *font); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromGlyphs) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[1]); + int bytesPerGlyph = 2; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + auto textBlob = SkTextBlob::MakeFromText(glyphs.data(), glyphs.size() * bytesPerGlyph, *font, SkTextEncoding::kGlyphID); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromRSXform) { + auto str = arguments[0].asString(runtime).utf8(runtime); + auto jsiRsxforms = arguments[1].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[2]); + int bytesPerGlyph = 2; + std::vector rsxforms; + int rsxformsSize = static_cast(jsiRsxforms.size(runtime)); + for (int i = 0; i < rsxformsSize; i++) { + auto rsxform = JsiSkRSXform::fromValue(runtime, jsiRsxforms.getValueAtIndex(runtime, i)); + rsxforms.push_back(*rsxform); + } + auto textBlob = SkTextBlob::MakeFromRSXform(str.c_str(), str.length(), rsxforms.data(), *font); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromRSXformGlyphs) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto jsiRsxforms = arguments[1].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[2]); + int bytesPerGlyph = 2; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + std::vector rsxforms; + int rsxformsSize = static_cast(jsiRsxforms.size(runtime)); + for (int i = 0; i < rsxformsSize; i++) { + auto rsxform = JsiSkRSXform::fromValue(runtime, jsiRsxforms.getValueAtIndex(runtime, i)); + rsxforms.push_back(*rsxform); + } + auto textBlob = SkTextBlob::MakeFromRSXform(glyphs.data(), glyphs.size() * bytesPerGlyph, rsxforms.data(), *font, SkTextEncoding::kGlyphID); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromText), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromGlyphs), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromRSXform), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromRSXformGlyphs), + ) + + JsiSkTextBlobFactory(std::shared_ptr context) + : JsiSkHostObject(context) {} + }; + +} // namespace RNSkia diff --git a/package/src/renderer/components/text/TextBlob.tsx b/package/src/renderer/components/text/TextBlob.tsx new file mode 100644 index 0000000000..2ce636f54d --- /dev/null +++ b/package/src/renderer/components/text/TextBlob.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type { CustomPaintProps, AnimatedProps } from "../../processors"; +import { useDrawing } from "../../nodes/Drawing"; +import type { ITextBlob } from "../../../skia/TextBlob"; + +export interface TextBlobProps extends CustomPaintProps { + blob: ITextBlob; + x: number; + y: number; +} + +export const TextBlob = (props: AnimatedProps) => { + const onDraw = useDrawing(props, ({ canvas, paint }, { blob, x, y }) => { + canvas.drawTextBlob(blob, x, y, paint); + }); + return ; +}; + +TextBlob.defaultProps = { + x: 0, + y: 0, +}; diff --git a/package/src/renderer/components/text/TextPath.tsx b/package/src/renderer/components/text/TextPath.tsx new file mode 100644 index 0000000000..36670c2653 --- /dev/null +++ b/package/src/renderer/components/text/TextPath.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +import type { CustomPaintProps, AnimatedProps } from "../../processors"; +import { useDrawing } from "../../nodes/Drawing"; +import type { IPath } from "../../../skia/Path"; +import type { IRSXform } from "../../../skia/RSXform"; +import { Skia } from "../../../skia/Skia"; +import type { FontDef } from "../../processors/Font"; +import { processFont } from "../../processors/Font"; + +export type TextPathProps = CustomPaintProps & + FontDef & { + text: string; + path: IPath | string; + initialOffset: number; + }; + +export const TextPath = (props: AnimatedProps) => { + const onDraw = useDrawing( + props, + ( + { canvas, paint, fontMgr }, + { text, initialOffset, path: pathDef, ...fontDef } + ) => { + const path = + typeof pathDef === "string" + ? Skia.Path.MakeFromSVGString(pathDef) + : pathDef; + if (path === null) { + throw new Error("Invalid path: " + pathDef); + } + const font = processFont(fontMgr, fontDef); + const ids = font.getGlyphIDs(text); + const widths = font.getGlyphWidths(ids, paint); + const rsx: IRSXform[] = []; + const meas = Skia.ContourMeasureIter(path, false, 1); + let cont = meas.next(); + let dist = initialOffset; + for (let i = 0; i < text.length && cont; i++) { + const width = widths[i]; + dist += width / 2; + if (dist > cont.length()) { + // jump to next contour + cont = meas.next(); + if (!cont) { + // We have come to the end of the path - terminate the string + // right here. + text = text.substring(0, i); + break; + } + dist = width / 2; + } + // Gives us the (x, y) coordinates as well as the cos/sin of the tangent + // line at that position. + const { px, py, tx, ty } = cont.getPosTan(dist); + const adjustedX = px - (width / 2) * tx; + const adjustedY = py - (width / 2) * ty; + rsx.push(Skia.RSXform(tx, ty, adjustedX, adjustedY)); + dist += width / 2; + } + const blob = Skia.TextBlob.MakeFromRSXform(text, rsx, font); + canvas.drawTextBlob(blob, 0, 0, paint); + } + ); + return ; +}; + +TextPath.defaultProps = { + initialOffset: 0, +}; diff --git a/package/src/renderer/components/text/index.ts b/package/src/renderer/components/text/index.ts index c3a24ae8c8..fa621bb50e 100644 --- a/package/src/renderer/components/text/index.ts +++ b/package/src/renderer/components/text/index.ts @@ -1,2 +1,4 @@ export * from "./Text"; export * from "./Glyphs"; +export * from "./TextBlob"; +export * from "./TextPath"; diff --git a/package/src/skia/Canvas.ts b/package/src/skia/Canvas.ts index 9ff43a2a5c..a7b4d46178 100644 --- a/package/src/skia/Canvas.ts +++ b/package/src/skia/Canvas.ts @@ -12,6 +12,7 @@ import type { IMatrix } from "./Matrix"; import type { IImageFilter } from "./ImageFilter"; import type { MipmapMode, FilterMode } from "./Image/Image"; import type { Vertices } from "./Vertices"; +import type { ITextBlob } from "./TextBlob"; export enum ClipOp { Difference, @@ -318,6 +319,16 @@ export interface ICanvas { */ drawText(str: string, x: number, y: number, paint: IPaint, font: IFont): void; + /** + * Draws the given TextBlob at (x, y) using the current clip, current matrix, and the + * provided paint. Reminder that the fonts used to draw TextBlob are part of the blob. + * @param blob + * @param x + * @param y + * @param paint + */ + drawTextBlob(blob: ITextBlob, x: number, y: number, paint: IPaint): void; + /** * Draws a run of glyphs, at corresponding positions, in a given font. * @param glyphs the array of glyph IDs (Uint16TypedArray) diff --git a/package/src/skia/ContourMeasure.tsx b/package/src/skia/ContourMeasure.tsx new file mode 100644 index 0000000000..eb79898344 --- /dev/null +++ b/package/src/skia/ContourMeasure.tsx @@ -0,0 +1,49 @@ +import type { SkJSIInstance } from "./JsiInstance"; +import type { IPath } from "./Path/Path"; + +export interface PosTan { + px: number; + py: number; + tx: number; + ty: number; +} + +export interface IContourMeasure extends SkJSIInstance<"ContourMeasureIter"> { + /** + * Returns the given position and tangent line for the distance on the given contour. + * The return value is 4 floats in this order: posX, posY, vecX, vecY. + * @param distance - will be pinned between 0 and length(). + * @param output - if provided, the four floats of the PosTan will be copied into this array + * instead of allocating a new one. + */ + getPosTan(distance: number, output?: PosTan): PosTan; + + /** + * Returns an Path representing the segement of this contour. + * @param startD - will be pinned between 0 and length() + * @param stopD - will be pinned between 0 and length() + * @param startWithMoveTo + */ + getSegment(startD: number, stopD: number, startWithMoveTo: boolean): IPath; + + /** + * Returns true if the contour is closed. + */ + isClosed(): boolean; + + /** + * Returns the length of this contour. + */ + length(): number; +} + +export interface IContourMeasureIter + extends SkJSIInstance<"ContourMeasureIter"> { + /** + * Iterates through contours in path, returning a contour-measure object for each contour + * in the path. Returns null when it is done. + * + * See SkContourMeasure.h for more details. + */ + next(): IContourMeasure | null; +} diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 2922ebe732..bf172285a2 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -39,6 +39,20 @@ export interface IFont extends SkJSIInstance<"Font"> { */ getGlyphIDs(str: string, numCodePoints?: number): number[]; + /** + * Retrieves the advanceX measurements for each glyph. + * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected. + * One width per glyph is returned in the returned array. + * @param glyphs + * @param paint + * @param output - if provided, the results will be copied into this array. + */ + getGlyphWidths( + glyphs: number[], + paint?: IPaint | null, + output?: Float32Array + ): Float32Array; + /** * Computes any intersections of a thick "line" and a run of positionsed glyphs. * The thick line is represented as a top and bottom coordinate (positive for diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index c554361832..a65d1922c2 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -18,10 +18,13 @@ import type { IPoint } from "./Point"; import type { Vertices, VertexMode } from "./Vertices/Vertices"; import type { DataFactory } from "./Data"; import type { SVGFactory } from "./SVG"; +import type { TextBlobFactory } from "./TextBlob"; import type { FontMgrFactory } from "./FontMgr/FontMgrFactory"; import type { SurfaceFactory } from "./Surface"; import "./NativeSetup"; import type { IRSXform } from "./RSXform"; +import type { IPath } from "./Path/Path"; +import type { IContourMeasureIter } from "./ContourMeasure"; /** * Declares the interface for the native Skia API @@ -31,6 +34,11 @@ export interface Skia { XYWHRect: (x: number, y: number, width: number, height: number) => IRect; RRectXY: (rect: IRect, rx: number, ry: number) => IRRect; RSXform: (scos: number, ssin: number, tx: number, ty: number) => IRSXform; + ContourMeasureIter: ( + path: IPath, + forceClosed: boolean, + resScale: number + ) => IContourMeasureIter; Paint: () => IPaint; Path: PathFactory; Matrix: () => IMatrix; @@ -64,6 +72,7 @@ export interface Skia { Image: ImageFactory; SVG: SVGFactory; FontMgr: FontMgrFactory; + TextBlob: TextBlobFactory; Surface: SurfaceFactory; } @@ -78,13 +87,7 @@ declare global { * Declares the implemented API with overrides. */ export const Skia = { - Point: SkiaApi.Point, - XYWHRect: SkiaApi.XYWHRect, - RRectXY: SkiaApi.RRectXY, - Paint: SkiaApi.Paint, - Path: SkiaApi.Path, - ColorFilter: SkiaApi.ColorFilter, - Font: SkiaApi.Font, + // Factories Typeface: SkiaApi.Typeface, MaskFilter: SkiaApi.MaskFilter, RuntimeEffect: SkiaApi.RuntimeEffect, @@ -92,14 +95,24 @@ export const Skia = { ImageFilter: SkiaApi.ImageFilter, PathEffect: SkiaApi.PathEffect, Data: SkiaApi.Data, - Matrix: SkiaApi.Matrix, - MakeVertices: SkiaApi.MakeVertices, SVG: SkiaApi.SVG, FontMgr: SkiaApi.FontMgr, + TextBlob: SkiaApi.TextBlob, + // Constructors + Matrix: SkiaApi.Matrix, + Font: SkiaApi.Font, + Point: SkiaApi.Point, + XYWHRect: SkiaApi.XYWHRect, + RRectXY: SkiaApi.RRectXY, + Paint: SkiaApi.Paint, + Path: SkiaApi.Path, + ColorFilter: SkiaApi.ColorFilter, + ContourMeasureIter: SkiaApi.ContourMeasureIter, // Here are constructors for data types which are represented as typed arrays in CanvasKit Color, RSXform: SkiaApi.RSXform, - // Here the factory symmetry is broken to be comptatible with CanvasKit + // For the following methods the factory symmetry is broken to be comptatible with CanvasKit MakeSurface: SkiaApi.Surface.Make, MakeImageFromEncoded: SkiaApi.Image.MakeImageFromEncoded, + MakeVertices: SkiaApi.MakeVertices, }; diff --git a/package/src/skia/TextBlob.ts b/package/src/skia/TextBlob.ts new file mode 100644 index 0000000000..1d93a8cbaf --- /dev/null +++ b/package/src/skia/TextBlob.ts @@ -0,0 +1,52 @@ +import type { SkJSIInstance } from "./JsiInstance"; +import type { IFont } from "./Font/Font"; +import type { IRSXform } from "./RSXform"; + +export type ITextBlob = SkJSIInstance<"TextBlob">; + +export interface TextBlobFactory { + /** + * Return a TextBlob with a single run of text. + * + * It uses the default character-to-glyph mapping from the typeface in the font. + * It does not perform typeface fallback for characters not found in the Typeface. + * It does not perform kerning or other complex shaping; glyphs are positioned based on their + * default advances. + * @param str + * @param font + */ + MakeFromText(str: string, font: IFont): ITextBlob; + /** + * Return a TextBlob with a single run of text. + * + * It does not perform typeface fallback for characters not found in the Typeface. + * It does not perform kerning or other complex shaping; glyphs are positioned based on their + * default advances. + * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs(). + * @param font + */ + MakeFromGlyphs(glyphs: number[], font: IFont): ITextBlob; + + /** + * Returns a TextBlob built from a single run of text with rotation, scale, and translations. + * + * It uses the default character-to-glyph mapping from the typeface in the font. + * @param str + * @param rsxforms + * @param font + */ + MakeFromRSXform(str: string, rsxforms: IRSXform[], font: IFont): ITextBlob; + + /** + * Returns a TextBlob built from a single run of text with rotation, scale, and translations. + * + * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs(). + * @param rsxforms + * @param font + */ + MakeFromRSXformGlyphs( + glyphs: number[], + rsxforms: IRSXform[], + font: IFont + ): ITextBlob; +}