Skip to content

Commit 73693f9

Browse files
authored
Path2D Implementation (#1469)
- https://developer.mozilla.org/en-US/docs/Web/API/Path2D Adds nanosvg.h for SVG path parsing. Known missing functionality: 1. `addPath` doesn't accept DOMMatrix transform 2. `roundRect` doesn't accept CSS-style radii array 3. `ellipse` doesn't handle clockwise Called from BabylonJS/Babylon.js#16221
1 parent e404270 commit 73693f9

File tree

6 files changed

+3507
-1
lines changed

6 files changed

+3507
-1
lines changed

Polyfills/Canvas/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ set(SOURCES
77
"Source/Image.h"
88
"Source/ImageData.cpp"
99
"Source/ImageData.h"
10+
"Source/Path2D.cpp"
11+
"Source/Path2D.h"
12+
"Source/LineCaps.h"
1013
"Source/Context.cpp"
1114
"Source/Context.h"
1215
"Source/MeasureText.cpp"
@@ -15,6 +18,7 @@ set(SOURCES
1518
"Source/Gradient.h"
1619
"Source/nanovg_babylon.cpp"
1720
"Source/nanovg_babylon.h"
21+
"Source/nanosvg.h"
1822
)
1923

2024
file(GLOB FONT_SOURCES ${BGFX_DIR}/examples/common/font/*.cpp)

Polyfills/Canvas/Source/Canvas.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "Canvas.h"
22
#include "Image.h"
3+
#include "Path2D.h"
34
#include "Context.h"
45
#include <bgfx/bgfx.h>
56
#include <napi/pointer.h>
@@ -236,6 +237,7 @@ namespace Babylon::Polyfills
236237

237238
Internal::NativeCanvas::CreateInstance(env);
238239
Internal::NativeCanvasImage::CreateInstance(env);
240+
Internal::NativeCanvasPath2D::CreateInstance(env);
239241
Internal::CanvasGradient::Initialize(env);
240242
Internal::Context::Initialize(env);
241243

Polyfills/Canvas/Source/Context.cpp

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "MeasureText.h"
2626
#include "Image.h"
2727
#include "ImageData.h"
28+
#include "Path2D.h"
2829
#include "Colors.h"
2930
#include "LineCaps.h"
3031
#include "Gradient.h"
@@ -354,8 +355,72 @@ namespace Babylon::Polyfills::Internal
354355
SetDirty();
355356
}
356357

357-
void Context::Stroke(const Napi::CallbackInfo&)
358+
void Context::Stroke(const Napi::CallbackInfo& info)
358359
{
360+
// draw Path2D if exists
361+
const NativeCanvasPath2D* path = info.Length() == 1 ? NativeCanvasPath2D::Unwrap(info[0].As<Napi::Object>()) : nullptr;
362+
if (path != nullptr)
363+
{
364+
nvgBeginPath(*m_nvg);
365+
for (const auto& command : *path)
366+
{
367+
const auto args = command.args;
368+
bool setDirty = true;
369+
switch (command.type)
370+
{
371+
case P2D_CLOSE:
372+
nvgClosePath(*m_nvg);
373+
break;
374+
case P2D_MOVETO:
375+
nvgMoveTo(*m_nvg, args.moveTo.x, args.moveTo.y);
376+
break;
377+
case P2D_LINETO:
378+
nvgLineTo(*m_nvg, args.lineTo.x, args.lineTo.y);
379+
break;
380+
case P2D_BEZIERTO:
381+
nvgBezierTo(*m_nvg, args.bezierTo.cp1x, args.bezierTo.cp1y,
382+
args.bezierTo.cp2x, args.bezierTo.cp2y,
383+
args.bezierTo.x, args.bezierTo.y);
384+
break;
385+
case P2D_QUADTO:
386+
nvgQuadTo(*m_nvg, args.quadTo.cpx, args.quadTo.cpy,
387+
args.quadTo.x, args.quadTo.y);
388+
break;
389+
case P2D_ARC:
390+
nvgArc(*m_nvg, args.arc.x, args.arc.y, args.arc.radius,
391+
args.arc.startAngle, args.arc.endAngle,
392+
args.arc.counterclockwise ? NVG_CCW : NVG_CW);
393+
break;
394+
case P2D_ARCTO:
395+
nvgArcTo(*m_nvg, args.arcTo.x1, args.arcTo.y1,
396+
args.arcTo.x2, args.arcTo.y2,
397+
args.arcTo.radius);
398+
break;
399+
case P2D_ELLIPSE:
400+
// TODO: handle clockwise for nvgElipse (args.ellipse.counterclockwise)
401+
nvgEllipse(*m_nvg, args.ellipse.x, args.ellipse.y,
402+
args.ellipse.radiusX, args.ellipse.radiusY);
403+
break;
404+
case P2D_RECT:
405+
nvgRect(*m_nvg, args.rect.x, args.rect.y,
406+
args.rect.width, args.rect.height);
407+
break;
408+
case P2D_ROUNDRECT:
409+
nvgRoundedRect(*m_nvg, args.roundRect.x, args.roundRect.y,
410+
args.roundRect.width, args.roundRect.height,
411+
args.roundRect.radii);
412+
break;
413+
default:
414+
setDirty = false; // noop
415+
break;
416+
}
417+
if (setDirty)
418+
{
419+
SetDirty();
420+
}
421+
}
422+
}
423+
359424
nvgStroke(*m_nvg);
360425
SetDirty();
361426
}

Polyfills/Canvas/Source/Path2D.cpp

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#include <bgfx/bgfx.h>
2+
#include <cassert>
3+
#include <map>
4+
#include "Canvas.h"
5+
#include "Path2D.h"
6+
#include <napi/pointer.h>
7+
8+
#ifdef __GNUC__
9+
#pragma GCC diagnostic push
10+
#pragma GCC diagnostic ignored "-Wpedantic"
11+
#endif
12+
13+
#include "nanovg.h"
14+
#define NANOSVG_IMPLEMENTATION // Expands implementation
15+
#include "nanosvg.h"
16+
17+
#ifdef __GNUC__
18+
#pragma GCC diagnostic pop
19+
#endif
20+
21+
namespace Babylon::Polyfills::Internal
22+
{
23+
static constexpr auto JS_PATH2D_CONSTRUCTOR_NAME = "Path2D";
24+
25+
void NativeCanvasPath2D::CreateInstance(Napi::Env env)
26+
{
27+
Napi::HandleScope scope{env};
28+
29+
Napi::Function func = DefineClass(
30+
env,
31+
JS_PATH2D_CONSTRUCTOR_NAME,
32+
{
33+
InstanceMethod("addPath", &NativeCanvasPath2D::AddPath),
34+
InstanceMethod("closePath", &NativeCanvasPath2D::ClosePath),
35+
InstanceMethod("moveTo", &NativeCanvasPath2D::MoveTo),
36+
InstanceMethod("lineTo", &NativeCanvasPath2D::LineTo),
37+
InstanceMethod("bezierCurveTo", &NativeCanvasPath2D::BezierCurveTo),
38+
InstanceMethod("quadraticCurveTo", &NativeCanvasPath2D::QuadraticCurveTo),
39+
InstanceMethod("arc", &NativeCanvasPath2D::Arc),
40+
InstanceMethod("arcTo", &NativeCanvasPath2D::ArcTo),
41+
InstanceMethod("ellipse", &NativeCanvasPath2D::Ellipse),
42+
InstanceMethod("rect", &NativeCanvasPath2D::Rect),
43+
InstanceMethod("roundRect", &NativeCanvasPath2D::RoundRect),
44+
});
45+
46+
JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_PATH2D_CONSTRUCTOR_NAME, func);
47+
}
48+
49+
NativeCanvasPath2D::NativeCanvasPath2D(const Napi::CallbackInfo& info)
50+
: Napi::ObjectWrap<NativeCanvasPath2D>{info}
51+
, m_commands{std::deque<Path2DCommand>()}
52+
{
53+
const std::string d = info.Length() == 1 ? info[0].As<Napi::String>().Utf8Value() : "";
54+
// auto context{info[0].As<Napi::External<NativeCanvasPath2D>>().Data()}; // TODO: Path2D constructor
55+
if (!d.empty())
56+
{
57+
NSVGparser* parser = nsvg__createParser();
58+
const char* path[] = {"d", d.c_str(), NULL};
59+
const char** attr = {path};
60+
61+
assert(strcmp(attr[0], "d") == 0);
62+
assert(!attr[2]); // nsvg__parsePath terminates attr parsing on falsy
63+
64+
nsvg__parsePath(parser, attr);
65+
66+
for (NSVGshape *shape = parser->image->shapes; shape != NULL; shape = shape->next) {
67+
for (NSVGpath *path = shape->paths; path != NULL; path = path->next) {
68+
for (int i = 0; i < path->npts-1; i += 3) {
69+
float* p = &path->pts[i*2];
70+
71+
auto x0 = p[0]; // start x, same as end x of previous
72+
auto y0 = p[1]; // start y, same as end y of previous
73+
auto cpx1 = p[2];
74+
auto cpy1 = p[3];
75+
auto cpx2 = p[4];
76+
auto cpy2 = p[5];
77+
auto x1 = p[6]; // end x
78+
auto y1 = p[7]; // end y
79+
80+
// Only need to move on new shape
81+
if (i == 0)
82+
{
83+
Path2DCommandArgs moveArgs = {};
84+
moveArgs.moveTo = {x0, y0};
85+
AppendCommand(P2D_MOVETO, moveArgs);
86+
}
87+
88+
Path2DCommandArgs args = {};
89+
args.bezierTo = {cpx1, cpy1, cpx2, cpy2, x1, y1};
90+
AppendCommand(P2D_BEZIERTO, args);
91+
}
92+
}
93+
}
94+
95+
nsvg__deleteParser(parser);
96+
}
97+
}
98+
99+
typename std::deque<Path2DCommand>::iterator NativeCanvasPath2D::begin()
100+
{
101+
return m_commands.begin();
102+
}
103+
104+
typename std::deque<Path2DCommand>::iterator NativeCanvasPath2D::end()
105+
{
106+
return m_commands.end();
107+
}
108+
109+
typename std::deque<Path2DCommand>::const_iterator NativeCanvasPath2D::begin() const
110+
{
111+
return m_commands.begin();
112+
}
113+
114+
typename std::deque<Path2DCommand>::const_iterator NativeCanvasPath2D::end() const
115+
{
116+
return m_commands.end();
117+
}
118+
119+
void NativeCanvasPath2D::AppendCommand(Path2DCommandTypes type, Path2DCommandArgs args)
120+
{
121+
m_commands.push_back({type, args});
122+
}
123+
124+
void NativeCanvasPath2D::AddPath(const Napi::CallbackInfo& info)
125+
{
126+
const NativeCanvasPath2D* path = NativeCanvasPath2D::Unwrap(info[0].As<Napi::Object>());
127+
// const NativeCanvasDOMMatrix path = NativeCanvasDOMMatrix::Unwrap(info[1].As<Napi::Object>()); // TODO: DOMMatrix
128+
129+
for (const auto& command : *path)
130+
{
131+
m_commands.push_back(command);
132+
}
133+
}
134+
135+
void NativeCanvasPath2D::ClosePath(const Napi::CallbackInfo& info)
136+
{
137+
AppendCommand(P2D_CLOSE, {});
138+
}
139+
140+
void NativeCanvasPath2D::MoveTo(const Napi::CallbackInfo& info)
141+
{
142+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
143+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
144+
145+
Path2DCommandArgs args = {};
146+
args.moveTo = {x, y};
147+
AppendCommand(P2D_MOVETO, args);
148+
}
149+
150+
void NativeCanvasPath2D::LineTo(const Napi::CallbackInfo& info)
151+
{
152+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
153+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
154+
155+
Path2DCommandArgs args = {};
156+
args.lineTo = {x, y};
157+
AppendCommand(P2D_LINETO, args);
158+
}
159+
160+
void NativeCanvasPath2D::BezierCurveTo(const Napi::CallbackInfo& info)
161+
{
162+
const auto cp1x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
163+
const auto cp1y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
164+
const auto cp2x = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
165+
const auto cp2y = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
166+
const auto x = static_cast<float>(info[4].As<Napi::Number>().DoubleValue());
167+
const auto y = static_cast<float>(info[5].As<Napi::Number>().DoubleValue());
168+
169+
Path2DCommandArgs args = {};
170+
args.bezierTo = {cp1x, cp1y, cp2x, cp2y, x, y};
171+
AppendCommand(P2D_BEZIERTO, args);
172+
}
173+
174+
void NativeCanvasPath2D::QuadraticCurveTo(const Napi::CallbackInfo& info)
175+
{
176+
const auto cpx = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
177+
const auto cpy = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
178+
const auto x = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
179+
const auto y = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
180+
181+
Path2DCommandArgs args = {};
182+
args.quadTo = {cpx, cpy, x, y};
183+
AppendCommand(P2D_QUADTO, args);
184+
}
185+
186+
void NativeCanvasPath2D::Arc(const Napi::CallbackInfo& info)
187+
{
188+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
189+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
190+
const auto radius = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
191+
const auto startAngle = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
192+
const auto endAngle = static_cast<float>(info[4].As<Napi::Number>().DoubleValue());
193+
const auto counterclockwise = info.Length() == 6 ? info[5].As<Napi::Boolean>() : false;
194+
195+
Path2DCommandArgs args = {};
196+
args.arc = {x, y, radius, startAngle, endAngle, counterclockwise};
197+
AppendCommand(P2D_ARC, args);
198+
}
199+
200+
void NativeCanvasPath2D::ArcTo(const Napi::CallbackInfo& info)
201+
{
202+
const auto x1 = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
203+
const auto y1 = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
204+
const auto x2 = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
205+
const auto y2 = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
206+
const auto radius = static_cast<float>(info[4].As<Napi::Number>().DoubleValue());
207+
208+
Path2DCommandArgs args = {};
209+
args.arcTo = {x1, y1, x2, y2, radius};
210+
AppendCommand(P2D_ARCTO, args);
211+
}
212+
213+
void NativeCanvasPath2D::Ellipse(const Napi::CallbackInfo& info)
214+
{
215+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
216+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
217+
const auto radiusX = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
218+
const auto radiusY = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
219+
const auto rotation = static_cast<float>(info[4].As<Napi::Number>().DoubleValue());
220+
const auto startAngle = static_cast<float>(info[5].As<Napi::Number>().DoubleValue());
221+
const auto endAngle = static_cast<float>(info[6].As<Napi::Number>().DoubleValue());
222+
const auto counterclockwise = info.Length() == 8 ? info[7].As<Napi::Boolean>() : false;
223+
224+
Path2DCommandArgs args = {};
225+
args.ellipse = {x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise};
226+
AppendCommand(P2D_ELLIPSE, args);
227+
}
228+
229+
void NativeCanvasPath2D::Rect(const Napi::CallbackInfo& info)
230+
{
231+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
232+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
233+
const auto width = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
234+
const auto height = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
235+
236+
Path2DCommandArgs args = {};
237+
args.rect = {x, y, width, height};
238+
AppendCommand(P2D_RECT, args);
239+
}
240+
241+
void NativeCanvasPath2D::RoundRect(const Napi::CallbackInfo& info)
242+
{
243+
const auto x = static_cast<float>(info[0].As<Napi::Number>().DoubleValue());
244+
const auto y = static_cast<float>(info[1].As<Napi::Number>().DoubleValue());
245+
const auto width = static_cast<float>(info[2].As<Napi::Number>().DoubleValue());
246+
const auto height = static_cast<float>(info[3].As<Napi::Number>().DoubleValue());
247+
const auto radii = static_cast<float>(info[4].As<Napi::Number>().DoubleValue()); // TODO: support list of numbers
248+
249+
Path2DCommandArgs args = {};
250+
args.roundRect = {x, y, width, height, radii};
251+
AppendCommand(P2D_ROUNDRECT, args);
252+
}
253+
}

0 commit comments

Comments
 (0)