diff --git a/.github/jobs/test_install_win32.yml b/.github/jobs/test_install_win32.yml
index a35f63a4b..07037d7ad 100644
--- a/.github/jobs/test_install_win32.yml
+++ b/.github/jobs/test_install_win32.yml
@@ -41,7 +41,6 @@ jobs:
# BGFX_CONFIG_MAX_FRAME_BUFFERS is set so enough Framebuffers are available before V8 starts disposing unused ones
- script: |
- # BGFX_CONFIG_MAX_FRAME_BUFFERS is set so enough Framebuffers are available before V8 starts disposing unused ones
cmake -B build${{ variables.solutionName }} -A ${{ parameters.platform }} ${{ variables.jsEngineDefine }} -D BX_CONFIG_DEBUG=ON -D GRAPHICS_API=${{ parameters.graphics_api }} -D CMAKE_UNITY_BUILD=$(UNITY_BUILD) -D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 -D BABYLON_DEBUG_TRACE=ON
displayName: 'Generate ${{ variables.solutionName }} solution'
diff --git a/Apps/Playground/Scripts/experience.js b/Apps/Playground/Scripts/experience.js
index aaa0b47bc..15241e93b 100644
--- a/Apps/Playground/Scripts/experience.js
+++ b/Apps/Playground/Scripts/experience.js
@@ -21,7 +21,7 @@ const imageTracking = false;
const readPixels = false;
function CreateBoxAsync(scene) {
- BABYLON.Mesh.CreateBox("box1", 0.2, scene);
+ //BABYLON.Mesh.CreateBox("box1", 0.2, scene);
return Promise.resolve();
}
@@ -64,6 +64,222 @@ CreateBoxAsync(scene).then(function () {
//BABYLON.SceneLoader.AppendAsync("https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/ClearCoatTest/glTF/ClearCoatTest.gltf").then(function () {
BABYLON.Tools.Log("Loaded");
+ var textureGround;
+
+ BABYLON.Tools.LoadFile("https://raw.githubusercontent.com/CedricGuillemet/dump/master/droidsans.ttf", (data) => {
+ _native.Canvas.loadTTFAsync("droidsans", data).then(function () {
+ var ground = BABYLON.MeshBuilder.CreateGround("ground1", { width: 0.5, height: 0.5, subdivisions: 2 }, scene);
+ ground.rotation.x = -Math.PI * 0.5;
+ ground.rotation.y = Math.PI;
+
+ var texSize = 512;
+ var dynamicTexture = new BABYLON.DynamicTexture("dynamic texture", texSize, scene);
+
+ var materialGround = new BABYLON.StandardMaterial("Mat", scene);
+ materialGround.diffuseTexture = dynamicTexture;
+ ground.material = materialGround;
+ materialGround.backFaceCulling = false;
+ dynamicTexture.clear();
+ var context = dynamicTexture.getContext();
+
+ // Text with gradient
+ const gradientText = context.createLinearGradient(0, 0, 256, 0);
+ gradientText.addColorStop(0, "magenta");
+ gradientText.addColorStop(0.5, "blue");
+ gradientText.addColorStop(1.0, "red");
+
+ // gradient
+ let gradient = context.createLinearGradient(0, 0, 200, 0);
+ gradient.addColorStop(0, "green");
+ gradient.addColorStop(0.7, "white");
+ gradient.addColorStop(1, "pink");
+
+ var t = 0;
+ // svg image
+ /*
+
+ */
+ const base64SVG = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0icmVkIiAvPgo8L3N2Zz4=`;
+
+ const svgImg = engine.createCanvasImage();
+ svgImg.onerror = function (e) {
+ console.error('Image load error:', e);
+ };
+ svgImg.src = base64SVG;
+ svgImg.onload = function () {
+
+ scene.onBeforeRenderObservable.add(() => {
+ // animated shape
+ context.save();
+ context.fillStyle = "DarkRed";
+ context.fillRect(0, 0, texSize, texSize);
+/*
+ const left = 0;
+ const top = texSize - (texSize * 0.25);
+ const width = 0.25 * texSize;
+ const height = 0.25 * texSize;
+ const offsetU = ((Math.sin(t) * 0.5) + 0.5) * (texSize - (texSize * 0.25));
+ const offsetV = ((Math.sin(t) * 0.5) + 0.5) * (-texSize + (texSize * 0.25));
+ const rectangleU = width * 0.5 + left;
+ const rectangleV = height * 0.5 + top;
+ context.translate(rectangleU + offsetU, rectangleV + offsetV);
+ context.rotate(t);
+ context.fillStyle = "DarkOrange";
+ context.transform(1, t, 0.8, 1, 0, 0);
+ context.fillRect(-width * 0.5, -height * 0.5, width, height);
+ context.restore();
+
+ // curve
+ context.beginPath();
+ context.moveTo(75 * 2, 25 * 2);
+ context.quadraticCurveTo(25 * 2, 25 * 2, 25 * 2, 62.5 * 2);
+ context.quadraticCurveTo(25 * 2, 100 * 2, 50 * 2, 100 * 2);
+ context.quadraticCurveTo(50 * 2, 120 * 2, 30 * 2, 125 * 2);
+ context.quadraticCurveTo(60 * 2, 120 * 2, 65 * 2, 100 * 2);
+ context.quadraticCurveTo(125 * 2, 100 * 2, 125 * 2, 62.5 * 2);
+ context.quadraticCurveTo(125 * 2, 25 * 2, 75 * 2, 25 * 2);
+ context.fillStyle = "blue";
+ context.fill();
+
+ // text
+ var scale = Math.sin(t) * 0.5 + 0.54;
+ context.save();
+ context.translate(Math.cos(t) * 100, 246);
+ context.font = `bold ${scale * 200}px monospace`;
+ context.strokeStyle = "Green";
+ context.lineWidth = scale * 16;
+ context.strokeText("BabylonNative", 0, 0);
+ context.fillStyle = "White";
+ context.fillText("BabylonNative", 0, 0);
+ context.restore();
+
+ // Draw guides
+ context.strokeStyle = "#09f";
+ context.beginPath();
+ context.moveTo(10, 10);
+ context.lineTo(140, 10);
+ context.moveTo(10, 140);
+ context.lineTo(140, 140);
+ context.stroke();
+
+ // filter blur text
+ context.filter = "blur(2.5px)";
+ context.fillStyle = "White";
+ context.font = `bold ${50}px monospace`;
+ context.fillText("BLUR TEST BLUR TEST", 100, 246);
+ context.filter = "none";
+
+ // Draw lines
+ context.strokeStyle = "black";
+ ["butt", "round", "square"].forEach((lineCap, i) => {
+ context.lineWidth = 15;
+ context.lineCap = lineCap;
+ context.beginPath();
+ context.moveTo(25 + i * 50, 10);
+ context.lineTo(25 + i * 50, 140);
+ context.stroke();
+ });
+
+ // line join
+ context.lineWidth = 10;
+ var offset = 200;
+ ["round", "bevel", "miter"].forEach((join, i) => {
+ context.lineJoin = join;
+ context.beginPath();
+ context.moveTo(-5 + offset, 15 + i * 40);
+ context.lineTo(35 + offset, 55 + i * 40);
+ context.lineTo(75 + offset, 15 + i * 40);
+ context.lineTo(115 + offset, 55 + i * 40);
+ context.lineTo(155 + offset, 15 + i * 40);
+ context.stroke();
+ });
+
+ // rect with gradient
+ context.fillStyle = gradient;
+ context.fillRect(10, 310, 400, 60);
+
+ // Fill with gradient
+ context.fillStyle = gradientText;
+ context.font = "bold 60px monospace";
+ context.fillText("Gradient Text!", 10, 420);
+
+ context.lineWidth = 5;
+ // Rounded rectangle with zero radius (specified as a number)
+ context.strokeStyle = "red";
+ context.beginPath();
+ context.roundRect(10, 220, 150, 100, 0);
+ context.stroke();
+
+ // Rounded rectangle with 40px radius (single element list)
+ context.strokeStyle = "blue";
+ context.beginPath();
+ context.roundRect(10, 220, 150, 100, [40]);
+ context.stroke();
+
+ // Rounded rectangle with 2 different radii
+ context.strokeStyle = "orange";
+ context.beginPath();
+ context.roundRect(10, 350, 150, 100, [10, 40]);
+ context.stroke();
+
+ // Rounded rectangle with four different radii
+ context.strokeStyle = "green";
+ context.beginPath();
+ context.roundRect(200, 220, 200, 100, [0, 30, 50, 60]);
+ context.stroke();
+
+ // Same rectangle drawn backwards
+ context.strokeStyle = "magenta";
+ context.beginPath();
+ context.roundRect(400, 350, -200, 100, [0, 30, 50, 60]);
+ context.stroke();
+
+ // Path 2D stroke
+ context.strokeStyle = "black";
+ context.lineWidth = 2;
+ let heartPath = new engine.createCanvasPath2D("M390,30 A 20, 20 0, 0, 1 430, 30 A 20, 20 0, 0, 1 470, 30 Q 470, 60 430, 90 Q 390, 60 390, 30 z");
+ let squarePath = new engine.createCanvasPath2D("M380, 10 h100 v100 h-100 Z");
+ heartPath.addPath(squarePath, { a: 1, b: 0, c: 0, d: 1, e: 0, f: -5 }); // push square 5px up to center heart.
+ context.stroke(heartPath);
+
+ // Path 2D fill
+ context.fillStyle = "yellow";
+ let diamondPath = new engine.createCanvasPath2D();
+ diamondPath.moveTo(350, 200); // Start at the center
+ diamondPath.lineTo(375, 175); // Move to the top point
+ diamondPath.lineTo(400, 200); // Move to the right point
+ diamondPath.lineTo(375, 225); // Move to the bottom point
+ diamondPath.lineTo(350, 200); // Close back to the starting point
+ context.fill(diamondPath);
+
+ // Path 2D round rect
+ context.strokeStyle = "red";
+ let roundRectPath = new engine.createCanvasPath2D();
+ roundRectPath.roundRect(300, 150, 45, 70, { x: 30, y: 15 });
+ context.stroke(roundRectPath);
+
+ // Draw clipped round rect
+ // TODO: this is currently broken, clipping area does not have round corners
+ context.beginPath();
+ context.roundRect(40, 450, 100, 50, 10);
+ context.clip();
+ context.fillStyle = "blue";
+ context.fillRect(0, 0, 1000, 1000);
+ */
+
+ // Draw the SVG on the canvas
+ context.drawImage(svgImg, 0, 0);
+
+ // tick update
+ dynamicTexture.update();
+ t += 0.01;
+ });
+ };
+ });
+ }, undefined, undefined, true);
+
// This creates and positions a free camera (non-mesh)
scene.createDefaultCamera(true, true, true);
scene.activeCamera.alpha += Math.PI;
diff --git a/Apps/UnitTests/Scripts/tests.js b/Apps/UnitTests/Scripts/tests.js
index 134e0d96f..8b7959136 100644
--- a/Apps/UnitTests/Scripts/tests.js
+++ b/Apps/UnitTests/Scripts/tests.js
@@ -1,4 +1,5 @@
-mocha.setup({ ui: "bdd", reporter: "spec", retries: 5 });
+"use strict";
+mocha.setup({ ui: "bdd", reporter: "spec", retries: 5 });
const expect = chai.expect;
@@ -15,6 +16,20 @@ describe("RequestFile", function () {
});
});
+describe("CanvasAndContext", function () {
+ const engine = new BABYLON.NativeEngine();
+ const scene = new BABYLON.Scene(engine);
+
+ const texSize = 512;
+ const dynamicTexture = new BABYLON.DynamicTexture("dynamic texture", texSize, scene);
+ const context = dynamicTexture.getContext();
+ const otherContext = dynamicTexture.getContext();
+
+ expect(context).to.equal(context.canvas.getContext());
+ expect(context).to.equal(otherContext);
+ expect(context).to.equal(otherContext.canvas.getContext());
+});
+
describe("ColorParsing", function () {
expect(_native.Canvas.parseColor("")).to.equal(0);
expect(_native.Canvas.parseColor("transparent")).to.equal(0);
diff --git a/Plugins/NativeEngine/CMakeLists.txt b/Plugins/NativeEngine/CMakeLists.txt
index 2b9e267bf..3b269230c 100644
--- a/Plugins/NativeEngine/CMakeLists.txt
+++ b/Plugins/NativeEngine/CMakeLists.txt
@@ -35,20 +35,40 @@ target_include_directories(NativeEngine
PUBLIC "Include"
PRIVATE "${BIMG_DIR}/3rdparty")
-target_link_libraries(NativeEngine
- PUBLIC napi
- PRIVATE arcana
- PRIVATE bgfx
- PRIVATE bimg
- PRIVATE bimg_encode
- PRIVATE bimg_decode
- PRIVATE bx
- PRIVATE glslang
- PRIVATE glslang-default-resource-limits
- PRIVATE GraphicsDevice
- PRIVATE GraphicsDeviceContext
- PRIVATE JsRuntime
- PRIVATE SPIRV)
+if(BGFX_BUILD_TOOLS)
+ target_link_libraries(NativeEngine
+ PUBLIC napi
+ PRIVATE arcana
+ PRIVATE bgfx
+ PRIVATE bimg
+ PRIVATE bimg_encode
+ PRIVATE bimg_decode
+ PRIVATE bx
+ PRIVATE glslang
+ PRIVATE GraphicsDevice
+ PRIVATE GraphicsDeviceContext
+ PRIVATE JsRuntime
+ PRIVATE spirv-opt)
+
+ target_compile_definitions(NativeEngine PRIVATE BGFX_BUILD_TOOLS)
+else()
+ target_link_libraries(NativeEngine
+ PUBLIC napi
+ PRIVATE arcana
+ PRIVATE bgfx
+ PRIVATE bimg
+ PRIVATE bimg_encode
+ PRIVATE bimg_decode
+ PRIVATE bx
+ PRIVATE glslang
+ PRIVATE glslang-default-resource-limits
+ PRIVATE GraphicsDevice
+ PRIVATE GraphicsDeviceContext
+ PRIVATE JsRuntime
+ PRIVATE SPIRV)
+
+ warnings_as_errors(NativeEngine)
+endif()
if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_WEBP)
target_compile_definitions(NativeEngine
diff --git a/Plugins/NativeEngine/Source/ShaderCompilerMetal.cpp b/Plugins/NativeEngine/Source/ShaderCompilerMetal.cpp
index e8a13c023..5b5241024 100644
--- a/Plugins/NativeEngine/Source/ShaderCompilerMetal.cpp
+++ b/Plugins/NativeEngine/Source/ShaderCompilerMetal.cpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/Plugins/NativeEngine/Source/ShaderCompilerTraversers.cpp b/Plugins/NativeEngine/Source/ShaderCompilerTraversers.cpp
index f3d0633c1..b272acdeb 100644
--- a/Plugins/NativeEngine/Source/ShaderCompilerTraversers.cpp
+++ b/Plugins/NativeEngine/Source/ShaderCompilerTraversers.cpp
@@ -220,8 +220,11 @@ namespace Babylon::ShaderCompilerTraversers
// Create the symbol for the actual struct. The name of this symbol, "anon@0",
// mirrors the kinds of strings glslang generates automatically for these sorts
// of objects.
- TIntermSymbol* structSymbol = intermediate->addSymbol(TIntermSymbol{ids.Next(), "anon@0", structType});
-
+#ifdef BGFX_BUILD_TOOLS
+ TIntermSymbol* structSymbol = intermediate->addSymbol(TIntermSymbol{ids.Next(), "anon@0", intermediate->getStage(), structType});
+#else
+ TIntermSymbol* structSymbol = intermediate->addSymbol(TIntermSymbol{ ids.Next(), "anon@0", structType });
+#endif
// Every affected symbol in the AST (except linker objects) must be replaced
// with a new operation to retrieve its value from the struct. This operation
// consists of a binary operation indexing into the struct at a specified
@@ -521,7 +524,11 @@ namespace Babylon::ShaderCompilerTraversers
TType newType{publicType};
newType.setBasicType(symbol->getType().getBasicType());
- auto* newSymbol = intermediate->addSymbol(TIntermSymbol{ids.Next(), newName, newType});
+#ifdef BGFX_BUILD_TOOLS
+ auto* newSymbol = intermediate->addSymbol(TIntermSymbol{ids.Next(), newName, intermediate->getStage(), newType});
+#else
+ auto* newSymbol = intermediate->addSymbol(TIntermSymbol{ ids.Next(), newName, newType });
+#endif
originalNameToReplacement[name] = newSymbol;
replacementToOriginalName[newName] = name;
}
@@ -794,7 +801,11 @@ namespace Babylon::ShaderCompilerTraversers
TType newType{publicType};
std::string newName = name + "Texture";
- newTexture = intermediate->addSymbol(TIntermSymbol{ids.Next(), newName.c_str(), newType});
+#ifdef BGFX_BUILD_TOOLS
+ newTexture = intermediate->addSymbol(TIntermSymbol{ids.Next(), newName.c_str(), intermediate->getStage(), newType});
+#else
+ newTexture = intermediate->addSymbol(TIntermSymbol{ ids.Next(), newName.c_str(), newType });
+#endif
}
// Create the new sampler symbol.
@@ -809,7 +820,11 @@ namespace Babylon::ShaderCompilerTraversers
publicType.sampler.sampler = true;
TType newType{publicType};
- newSampler = intermediate->addSymbol(TIntermSymbol{ids.Next(), name.c_str(), newType});
+#ifdef BGFX_BUILD_TOOLS
+ newSampler = intermediate->addSymbol(TIntermSymbol{ids.Next(), name.c_str(), intermediate->getStage(), newType});
+#else
+ newSampler = intermediate->addSymbol(TIntermSymbol{ ids.Next(), name.c_str(), newType });
+#endif
}
nameToNewTextureAndSampler[name] = std::pair{newTexture, newSampler};
diff --git a/Polyfills/Canvas/CMakeLists.txt b/Polyfills/Canvas/CMakeLists.txt
index be6b3c514..45c2c3329 100644
--- a/Polyfills/Canvas/CMakeLists.txt
+++ b/Polyfills/Canvas/CMakeLists.txt
@@ -18,27 +18,29 @@ function(add_bgfx_shader FILE FOLDER)
set(OUTPUTS_PRETTY "")
# dx11
- set(DX11_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/dx11/${FILENAME}.h)
- if(NOT "${TYPE}" STREQUAL "COMPUTE")
- _bgfx_shaderc_parse(
- DX11 ${COMMON} WINDOWS
- PROFILE s_5_0
- O 3
- OUTPUT ${DX11_OUTPUT}
- BIN2C "${FILENAME}_dx11"
- )
- else()
- _bgfx_shaderc_parse(
- DX11 ${COMMON} WINDOWS
- PROFILE s_5_0
- O 1
- OUTPUT ${DX11_OUTPUT}
- BIN2C "${FILENAME}_dx11"
- )
+ if(WIN32)
+ set(DX11_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/dx11/${FILENAME}.h)
+ if(NOT "${TYPE}" STREQUAL "COMPUTE")
+ _bgfx_shaderc_parse(
+ DX11 ${COMMON} WINDOWS
+ PROFILE s_5_0
+ O 3
+ OUTPUT ${DX11_OUTPUT}
+ BIN2C "${FILENAME}_dx11"
+ )
+ else()
+ _bgfx_shaderc_parse(
+ DX11 ${COMMON} WINDOWS
+ PROFILE s_5_0
+ O 1
+ OUTPUT ${DX11_OUTPUT}
+ BIN2C "${FILENAME}_dx11"
+ )
+ endif()
+ list(APPEND OUTPUTS "DX11")
+ set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}DX11, ")
endif()
- list(APPEND OUTPUTS "DX11")
- set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}DX11, ")
-
+
# metal
set(METAL_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/metal/${FILENAME}.h)
_bgfx_shaderc_parse(METAL ${COMMON} OSX PROFILE metal OUTPUT ${METAL_OUTPUT} BIN2C "${FILENAME}_mtl")
@@ -93,32 +95,41 @@ set(SOURCES
"Source/Canvas.cpp"
"Source/Canvas.h"
"Source/Colors.h"
+ "Source/FrameBufferPool.cpp"
+ "Source/FrameBufferPool.h"
"Source/Image.cpp"
"Source/Image.h"
"Source/ImageData.cpp"
"Source/ImageData.h"
+ "Source/Path2D.cpp"
+ "Source/Path2D.h"
+ "Source/LineCaps.h"
"Source/Context.cpp"
"Source/Context.h"
"Source/MeasureText.cpp"
"Source/MeasureText.h"
- "Source/nanovg_babylon.cpp"
- "Source/nanovg_babylon.h"
+ "Source/Gradient.cpp"
+ "Source/Gradient.h"
+ "Source/Font.cpp"
+ "Source/Font.h"
+ "Source/nanosvg.h"
+ "Source/nanosvgrast.h"
+ "Source/nanovg/nanovg.cpp"
+ "Source/nanovg/nanovg.h"
+ "Source/nanovg/nanovg_babylon.cpp"
+ "Source/nanovg/nanovg_babylon.h"
+ "Source/nanovg/nanovg_filterstack.cpp"
+ "Source/nanovg/nanovg_filterstack.h"
)
file(GLOB SHADERS "Source/Shaders/*.sc" "Source/Shaders/*.sh")
-file(GLOB FONT_SOURCES ${BGFX_DIR}/examples/common/font/*.cpp)
-file(GLOB NANOVG_SOURCES ${BGFX_DIR}/examples/common/nanovg/nanovg.cpp)
-set(ATLAS_SOURCES ${BGFX_DIR}/examples/common/cube_atlas.cpp)
-
-add_library(Canvas ${SOURCES} ${FONT_SOURCES} ${ATLAS_SOURCES} ${NANOVG_SOURCES} ${SHADERS})
+add_library(Canvas ${SOURCES} ${SHADERS})
target_include_directories(Canvas
PUBLIC "Include"
PRIVATE "Source"
- PRIVATE "${BGFX_DIR}/3rdparty"
- PRIVATE "${BGFX_DIR}/examples/common"
- PRIVATE "${BGFX_DIR}/examples/common/nanovg")
+ PRIVATE "${BGFX_DIR}/3rdparty")
target_link_libraries(Canvas
PUBLIC napi
@@ -140,7 +151,8 @@ if(BGFX_BUILD_TOOLS AND BGFX_BUILD_TOOLS_SHADER)
endif()
set_property(TARGET Canvas PROPERTY FOLDER Polyfills)
+set_property(TARGET Canvas PROPERTY UNITY_BUILD false)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
-source_group("3rd party Sources" ${CMAKE_CURRENT_SOURCE_DIR} FILES ${FONT_SOURCES} ${NANOVG_SOURCES} ${ATLAS_SOURCES})
+source_group("3rd party Sources" ${CMAKE_CURRENT_SOURCE_DIR} FILES ${FONT_SOURCES} ${ATLAS_SOURCES})
source_group("Shaders" ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SHADERS})
-target_compile_definitions(Canvas PRIVATE _CRT_SECURE_NO_WARNINGS)
\ No newline at end of file
+target_compile_definitions(Canvas PRIVATE _CRT_SECURE_NO_WARNINGS)
diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp
index 594697908..74c0d51be 100644
--- a/Polyfills/Canvas/Source/Canvas.cpp
+++ b/Polyfills/Canvas/Source/Canvas.cpp
@@ -1,10 +1,12 @@
#include "Canvas.h"
#include "Image.h"
+#include "Path2D.h"
#include "Context.h"
#include
#include
#include
#include "Colors.h"
+#include "Gradient.h"
namespace
{
@@ -23,6 +25,7 @@ namespace Babylon::Polyfills::Internal
env,
JS_CONSTRUCTOR_NAME,
{
+ StaticMethod("loadTTF", &NativeCanvas::LoadTTF),
StaticMethod("loadTTFAsync", &NativeCanvas::LoadTTFAsync),
InstanceAccessor("width", &NativeCanvas::GetWidth, &NativeCanvas::SetWidth),
InstanceAccessor("height", &NativeCanvas::GetHeight, &NativeCanvas::SetHeight),
@@ -57,22 +60,27 @@ namespace Babylon::Polyfills::Internal
// called when removed from document which has no meaning for Native
}
+ void NativeCanvas::LoadTTF(const Napi::CallbackInfo& info)
+ {
+ // don't allow same font to be loaded more than once
+ // why? because Context doesn't update nvgCreateFontMem when old fontBuffer released
+ auto fontName = info[0].As().Utf8Value();
+ if (fontsInfos.find(fontName) == fontsInfos.end())
+ {
+ const auto buffer = info[1].As();
+ std::vector fontBuffer(buffer.ByteLength());
+ memcpy(fontBuffer.data(), (uint8_t*)buffer.Data(), buffer.ByteLength());
+ fontsInfos[fontName] = std::move(fontBuffer);
+ }
+ }
+
+ // @deprecated: LoadTTFAsync is always synchronous, use LoadTTF instead
Napi::Value NativeCanvas::LoadTTFAsync(const Napi::CallbackInfo& info)
{
- const auto buffer = info[1].As();
- std::vector fontBuffer(buffer.ByteLength());
- memcpy(fontBuffer.data(), (uint8_t*)buffer.Data(), buffer.ByteLength());
+ LoadTTF(info);
- auto& graphicsContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())};
- auto update = graphicsContext.GetUpdate("update");
- std::shared_ptr runtimeScheduler{std::make_shared(JsRuntime::GetFromJavaScript(info.Env()))};
auto deferred{Napi::Promise::Deferred::New(info.Env())};
- arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName{info[0].As().Utf8Value()}, fontData{std::move(fontBuffer)}]() {
- fontsInfos[fontName] = fontData;
- }).then(*runtimeScheduler, arcana::cancellation::none(), [runtimeScheduler /*Keep reference alive*/, env{info.Env()}, deferred]() {
- deferred.Resolve(env.Undefined());
- });
-
+ deferred.Resolve(info.Env().Undefined());
return deferred.Promise();
}
@@ -99,7 +107,16 @@ namespace Babylon::Polyfills::Internal
void NativeCanvas::SetWidth(const Napi::CallbackInfo&, const Napi::Value& value)
{
auto width = static_cast(value.As().Uint32Value());
- if (width != m_width && width)
+ if (!width)
+ {
+ return;
+ }
+
+ if (width == m_width)
+ {
+ m_clear = true;
+ }
+ else
{
m_width = width;
m_dirty = true;
@@ -113,16 +130,29 @@ namespace Babylon::Polyfills::Internal
void NativeCanvas::SetHeight(const Napi::CallbackInfo&, const Napi::Value& value)
{
- auto height = value.As().Uint32Value();
- if (height != m_height && height)
+ auto height = static_cast(value.As().Uint32Value());
+ if (!height)
+ {
+ return;
+ }
+
+ if (height == m_height)
+ {
+ m_clear = true;
+ }
+ else
{
m_height = height;
m_dirty = true;
}
}
- void NativeCanvas::UpdateRenderTarget()
+ bool NativeCanvas::UpdateRenderTarget()
{
+ // in some scenarios (eg. no size change on SetSize/SetHeight) we can re-use framebuffer
+ bool needClear = m_clear;
+ m_clear = false;
+
if (m_dirty)
{
// make sure render targets are filled with 0 : https://registry.khronos.org/webgl/specs/latest/1.0/#TEXIMAGE2D
@@ -152,7 +182,15 @@ namespace Babylon::Polyfills::Internal
{
m_texture.reset();
}
+
+ m_frameBufferPool.Clear();
+ m_frameBufferPool.SetDimensions(m_width, m_height);
+ m_frameBufferPool.SetGraphicsContext(&m_graphicsContext);
+
+ return true;
}
+
+ return needClear;
}
Napi::Value NativeCanvas::GetCanvasTexture(const Napi::CallbackInfo& info)
@@ -178,6 +216,7 @@ namespace Babylon::Polyfills::Internal
{
m_frameBuffer.reset();
m_texture.reset();
+ m_frameBufferPool.Clear();
}
void NativeCanvas::Dispose(const Napi::CallbackInfo& /*info*/)
@@ -252,7 +291,8 @@ namespace Babylon::Polyfills
Internal::NativeCanvas::Initialize(env);
Internal::NativeCanvasImage::Initialize(env);
-
+ Internal::NativeCanvasPath2D::Initialize(env);
+ Internal::CanvasGradient::Initialize(env);
Internal::Context::Initialize(env);
return {impl};
diff --git a/Polyfills/Canvas/Source/Canvas.h b/Polyfills/Canvas/Source/Canvas.h
index 5de7441f0..d2f1d35e0 100644
--- a/Polyfills/Canvas/Source/Canvas.h
+++ b/Polyfills/Canvas/Source/Canvas.h
@@ -6,6 +6,8 @@
#include
#include
+#include "FrameBufferPool.h"
+
namespace Babylon::Polyfills
{
class Canvas::Impl final : public std::enable_shared_from_this
@@ -64,8 +66,9 @@ namespace Babylon::Polyfills::Internal
static inline std::map> fontsInfos;
- void UpdateRenderTarget();
+ bool UpdateRenderTarget();
Babylon::Graphics::FrameBuffer& GetFrameBuffer() { return *m_frameBuffer; }
+ FrameBufferPool m_frameBufferPool;
Graphics::DeviceContext& GetGraphicsContext()
{
@@ -79,12 +82,15 @@ namespace Babylon::Polyfills::Internal
Napi::Value GetHeight(const Napi::CallbackInfo&);
void SetHeight(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetCanvasTexture(const Napi::CallbackInfo& info);
+ static void LoadTTF(const Napi::CallbackInfo& info);
static Napi::Value LoadTTFAsync(const Napi::CallbackInfo& info);
static Napi::Value ParseColor(const Napi::CallbackInfo& info);
void Remove(const Napi::CallbackInfo& info);
void Dispose(const Napi::CallbackInfo& info);
void Dispose();
+ Napi::ObjectReference m_contextObject{};
+
uint16_t m_width{1};
uint16_t m_height{1};
@@ -93,6 +99,7 @@ namespace Babylon::Polyfills::Internal
std::unique_ptr m_frameBuffer;
std::unique_ptr m_texture{};
bool m_dirty{};
+ bool m_clear{};
void FlushGraphicResources() override;
};
diff --git a/Polyfills/Canvas/Source/Colors.h b/Polyfills/Canvas/Source/Colors.h
index fa131e529..7073fd2c9 100644
--- a/Polyfills/Canvas/Source/Colors.h
+++ b/Polyfills/Canvas/Source/Colors.h
@@ -1,6 +1,6 @@
#pragma once
#include
-#include "nanovg.h"
+#include "nanovg/nanovg.h"
namespace Babylon::Polyfills::Internal
{
diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp
index a1e45ebc3..823f9110a 100644
--- a/Polyfills/Canvas/Source/Context.cpp
+++ b/Polyfills/Canvas/Source/Context.cpp
@@ -10,7 +10,7 @@
#endif
#include "nanovg/nanovg.h"
-#include "nanovg_babylon.h"
+#include "nanovg/nanovg_babylon.h"
#ifdef __GNUC__
#pragma GCC diagnostic pop
@@ -25,7 +25,10 @@
#include "MeasureText.h"
#include "Image.h"
#include "ImageData.h"
+#include "Path2D.h"
#include "Colors.h"
+#include "LineCaps.h"
+#include "Gradient.h"
/*
Most of these context methods are preliminary work. They are currenbly not tested properly.
@@ -51,6 +54,7 @@ namespace Babylon::Polyfills::Internal
InstanceMethod("translate", &Context::Translate),
InstanceMethod("strokeRect", &Context::StrokeRect),
InstanceMethod("rect", &Context::Rect),
+ InstanceMethod("roundRect", &Context::RoundRect),
InstanceMethod("clip", &Context::Clip),
InstanceMethod("putImageData", &Context::PutImageData),
InstanceMethod("arc", &Context::Arc),
@@ -68,12 +72,19 @@ namespace Babylon::Polyfills::Internal
InstanceMethod("fillText", &Context::FillText),
InstanceMethod("strokeText", &Context::StrokeText),
InstanceMethod("createLinearGradient", &Context::CreateLinearGradient),
+ InstanceMethod("createRadialGradient", &Context::CreateRadialGradient),
+ InstanceMethod("getTransform", &Context::GetTransform),
InstanceMethod("setTransform", &Context::SetTransform),
+ InstanceMethod("transform", &Context::Transform),
InstanceMethod("dispose", &Context::Dispose),
InstanceMethod("flush", &Context::Flush),
+ InstanceAccessor("lineCap", &Context::GetLineCap, &Context::SetLineCap),
InstanceAccessor("lineJoin", &Context::GetLineJoin, &Context::SetLineJoin),
InstanceAccessor("miterLimit", &Context::GetMiterLimit, &Context::SetMiterLimit),
+ InstanceAccessor("filter", &Context::GetFilter, &Context::SetFilter),
+ InstanceAccessor("direction", &Context::GetDirection, &Context::SetDirection),
InstanceAccessor("font", &Context::GetFont, &Context::SetFont),
+ InstanceAccessor("letterSpacing", &Context::GetLetterSpacing, &Context::SetLetterSpacing),
InstanceAccessor("strokeStyle", &Context::GetStrokeStyle, &Context::SetStrokeStyle),
InstanceAccessor("fillStyle", &Context::GetFillStyle, &Context::SetFillStyle),
InstanceAccessor("globalAlpha", nullptr, &Context::SetGlobalAlpha),
@@ -95,7 +106,7 @@ namespace Babylon::Polyfills::Internal
Context::Context(const Napi::CallbackInfo& info)
: Napi::ObjectWrap{info}
, m_canvas{NativeCanvas::Unwrap(info[0].As())}
- , m_nvg{nvgCreate(1)}
+ , m_nvg{std::make_shared(nvgCreate(1))}
, m_graphicsContext{m_canvas->GetGraphicsContext()}
, m_update{m_graphicsContext.GetUpdate("update")}
, m_cancellationSource{std::make_shared()}
@@ -108,7 +119,8 @@ namespace Babylon::Polyfills::Internal
for (auto& font : NativeCanvas::fontsInfos)
{
- m_fonts[font.first] = nvgCreateFontMem(m_nvg, font.first.c_str(), font.second.data(), static_cast(font.second.size()), 0);
+ // TODO: update nvgCreateFontMem safely when old font buffer invalidated
+ m_fonts[font.first] = nvgCreateFontMem(*m_nvg, font.first.c_str(), font.second.data(), static_cast(font.second.size()), 0);
}
}
@@ -134,15 +146,46 @@ namespace Babylon::Polyfills::Internal
{
for (auto& image : m_nvgImageIndices)
{
- nvgDeleteImage(m_nvg, image.second);
+ nvgDeleteImage(*m_nvg, image.second);
}
- nvgDelete(m_nvg);
+ nvgDelete(*m_nvg);
m_nvg = nullptr;
}
m_isClipped = false;
}
+ void Context::BindFillStyle(const Napi::CallbackInfo& info, float left, float top, float width, float height)
+ {
+ if (std::holds_alternative(m_fillStyle))
+ {
+ const auto color = StringToColor(info.Env(), std::get(m_fillStyle));
+ nvgFillColor(*m_nvg, color);
+ }
+ else if (std::holds_alternative(m_fillStyle))
+ {
+ CanvasGradient* gradient = std::get(m_fillStyle);
+ gradient->UpdateCache();
+ // TODO: replace left/lop/width/height by context bounds
+ NVGpaint imagePaint = nvgImagePattern(*m_nvg, 0.f, 0.f, width + left, height, 0.f, gradient->CachedImage(), 1.f);
+ nvgFillPaint(*m_nvg, imagePaint);
+ }
+ else
+ {
+ throw Napi::Error::New(info.Env(), "Fillstyle is not a color string or a gradient.");
+ }
+ }
+
+ void Context::SetFilterStack()
+ {
+ if (m_filter.length())
+ {
+ nanovg_filterstack filterStack;
+ filterStack.ParseString(m_filter);
+ nvgFilterStack(*m_nvg, filterStack); // sets filterStack on nanovg
+ }
+ }
+
void Context::FillRect(const Napi::CallbackInfo& info)
{
auto left = info[0].As().FloatValue();
@@ -152,26 +195,43 @@ namespace Babylon::Polyfills::Internal
if (!m_isClipped)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
- nvgRect(m_nvg, left, top, width, height);
+ nvgRect(*m_nvg, left, top, width, height);
+
+ BindFillStyle(info, left, top, width, height);
- const auto color = StringToColor(info.Env(), m_fillStyle);
- nvgFillColor(m_nvg, color);
- nvgFill(m_nvg);
+ SetFilterStack();
+ nvgFill(*m_nvg);
}
Napi::Value Context::GetFillStyle(const Napi::CallbackInfo&)
{
- return Napi::Value::From(Env(), m_fillStyle);
+ if (std::holds_alternative(m_fillStyle))
+ {
+ return Napi::Value::From(Env(), std::get(m_fillStyle));
+ }
+ else
+ {
+ return Napi::External::New(Env(), std::get(m_fillStyle));
+ }
}
void Context::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value)
{
- m_fillStyle = value.As().Utf8Value();
- const auto color = StringToColor(info.Env(), m_fillStyle);
- nvgFillColor(m_nvg, color);
+ if (value.IsString())
+ {
+ auto string = value.As().Utf8Value();
+ const auto color = StringToColor(info.Env(), string);
+ m_fillStyle = string;
+ nvgFillColor(*m_nvg, color);
+ }
+ else
+ {
+ CanvasGradient* canvasGradient = CanvasGradient::Unwrap(info[0].As());
+ m_fillStyle = canvasGradient;
+ }
}
Napi::Value Context::GetStrokeStyle(const Napi::CallbackInfo&)
@@ -183,7 +243,7 @@ namespace Babylon::Polyfills::Internal
{
m_strokeStyle = value.As().Utf8Value();
auto color = StringToColor(info.Env(), m_strokeStyle);
- nvgStrokeColor(m_nvg, color);
+ nvgStrokeColor(*m_nvg, color);
}
Napi::Value Context::GetLineWidth(const Napi::CallbackInfo&)
@@ -194,22 +254,35 @@ namespace Babylon::Polyfills::Internal
void Context::SetLineWidth(const Napi::CallbackInfo&, const Napi::Value& value)
{
m_lineWidth = value.As().FloatValue();
- nvgStrokeWidth(m_nvg, m_lineWidth);
+ nvgStrokeWidth(*m_nvg, m_lineWidth);
}
- void Context::Fill(const Napi::CallbackInfo&)
+ void Context::Fill(const Napi::CallbackInfo& info)
{
- nvgFill(m_nvg);
+ SetFilterStack();
+
+ const NativeCanvasPath2D* path = info.Length() >= 1 && info[0].IsObject()
+ ? NativeCanvasPath2D::Unwrap(info[0].As())
+ : nullptr;
+ // TODO: handle fillRule: nonzero, evenodd
+
+ // draw Path2D if exists
+ if (path != nullptr)
+ {
+ PlayPath2D(path);
+ }
+
+ nvgFill(*m_nvg);
}
void Context::Save(const Napi::CallbackInfo&)
{
- nvgSave(m_nvg);
+ nvgSave(*m_nvg);
}
void Context::Restore(const Napi::CallbackInfo&)
{
- nvgRestore(m_nvg);
+ nvgRestore(*m_nvg);
m_isClipped = false;
}
@@ -220,54 +293,54 @@ namespace Babylon::Polyfills::Internal
const float width = info[2].As().FloatValue();
const float height = info[3].As().FloatValue();
- nvgSave(m_nvg);
- nvgGlobalCompositeOperation(m_nvg, NVG_COPY);
+ nvgSave(*m_nvg);
+ nvgGlobalCompositeOperation(*m_nvg, NVG_COPY);
if (!m_isClipped)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
- nvgRect(m_nvg, x, y, width, height);
+ nvgRect(*m_nvg, x, y, width, height);
if (!m_isClipped)
{
- nvgClosePath(m_nvg);
+ nvgClosePath(*m_nvg);
}
- nvgFillColor(m_nvg, TRANSPARENT_BLACK);
- nvgFill(m_nvg);
- nvgRestore(m_nvg);
+ nvgFillColor(*m_nvg, TRANSPARENT_BLACK);
+ nvgFill(*m_nvg);
+ nvgRestore(*m_nvg);
}
void Context::Translate(const Napi::CallbackInfo& info)
{
const auto x = info[0].As().FloatValue();
const auto y = info[1].As().FloatValue();
- nvgTranslate(m_nvg, x, y);
+ nvgTranslate(*m_nvg, x, y);
}
void Context::Rotate(const Napi::CallbackInfo& info)
{
const auto angle = info[0].As().FloatValue();
- nvgRotate(m_nvg, angle);
+ nvgRotate(*m_nvg, angle);
}
void Context::Scale(const Napi::CallbackInfo& info)
{
const auto x = info[0].As().FloatValue();
const auto y = info[1].As().FloatValue();
- nvgScale(m_nvg, x, y);
+ nvgScale(*m_nvg, x, y);
}
void Context::BeginPath(const Napi::CallbackInfo&)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
void Context::ClosePath(const Napi::CallbackInfo&)
{
- nvgClosePath(m_nvg);
+ nvgClosePath(*m_nvg);
}
void Context::Rect(const Napi::CallbackInfo& info)
@@ -277,10 +350,78 @@ namespace Babylon::Polyfills::Internal
const auto width = info[2].As().FloatValue();
const auto height = info[3].As().FloatValue();
- nvgRect(m_nvg, left, top, width, height);
+ nvgRect(*m_nvg, left, top, width, height);
m_rectangleClipping = {left, top, width, height};
}
+ void Context::RoundRect(const Napi::CallbackInfo& info)
+ {
+ const auto x = info[0].As().FloatValue();
+ const auto y = info[1].As().FloatValue();
+ const auto width = info[2].As().FloatValue();
+ const auto height = info[3].As().FloatValue();
+ const auto radii = info[4];
+
+ if (radii.IsNumber())
+ {
+ const auto radius = radii.As().FloatValue();
+ nvgRoundedRect(*m_nvg, x, y, width, height, radius);
+ }
+ else if (radii.IsArray())
+ {
+ const auto radiiArray = radii.As();
+ const auto radiiArrayLength = radiiArray.Length();
+ if (radiiArrayLength == 1)
+ {
+ const auto radius = radiiArray[0u].As().FloatValue();
+ nvgRoundedRect(*m_nvg, x, y, width, height, radius);
+ }
+ else if (radiiArrayLength == 2)
+ {
+ const auto topLeftBottomRight = radiiArray[0u].As().FloatValue();
+ const auto topRightBottomLeft = radiiArray[1].As().FloatValue();
+
+ nvgRoundedRectVarying(*m_nvg, x, y, width, height, topLeftBottomRight, topRightBottomLeft, topLeftBottomRight, topRightBottomLeft);
+ }
+ else if (radiiArrayLength == 3)
+ {
+ const auto topLeft = radiiArray[0u].As().FloatValue();
+ const auto topRightBottomLeft = radiiArray[1].As().FloatValue();
+ const auto bottomRight = radiiArray[2].As().FloatValue();
+
+ nvgRoundedRectVarying(*m_nvg, x, y, width, height, topLeft, topRightBottomLeft, bottomRight, topRightBottomLeft);
+ }
+ else if (radiiArrayLength == 4)
+ {
+ const auto topLeft = radiiArray[0u].As().FloatValue();
+ const auto topRight = radiiArray[1].As().FloatValue();
+ const auto bottomRight = radiiArray[2].As().FloatValue();
+ const auto bottomLeft = radiiArray[3].As().FloatValue();
+
+ nvgRoundedRectVarying(*m_nvg, x, y, width, height, topLeft, topRight, bottomRight, bottomLeft);
+ }
+ else
+ {
+ throw Napi::Error::New(info.Env(), "Invalid number of parameters for radii");
+ }
+ }
+ // DOMPoint
+ // TODO: move duplicate Path2D & Context args parsing into a utils.cpp
+ else if (radii.IsObject())
+ {
+ const auto dompoint = radii.As();
+ const auto dpx = dompoint.Get("x").As().FloatValue();
+ const auto dpy = dompoint.Get("y").As().FloatValue();
+ nvgRoundedRectElliptic(*m_nvg, x, y, width, height, dpx, dpy, dpx, dpy, dpx, dpy, dpx, dpy);
+ }
+ else
+ {
+ throw Napi::Error::New(info.Env(), "Invalid radii parameter");
+ }
+
+ m_rectangleClipping = {x, y, width, height};
+ }
+
void Context::Clip(const Napi::CallbackInfo& /*info*/)
{
m_isClipped = true;
@@ -290,7 +431,7 @@ namespace Babylon::Polyfills::Internal
auto h = m_rectangleClipping.height != 0 ? m_rectangleClipping.height : m_canvas->GetHeight();
// expand clipping 1pix in each direction because nanovg AA gets cut a bit short.
- nvgScissor(m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1);
+ nvgScissor(*m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1);
}
void Context::StrokeRect(const Napi::CallbackInfo& info)
@@ -300,13 +441,97 @@ namespace Babylon::Polyfills::Internal
const auto width = info[2].As().FloatValue();
const auto height = info[3].As().FloatValue();
- nvgRect(m_nvg, left, top, width, height);
- nvgStroke(m_nvg);
+ nvgRect(*m_nvg, left, top, width, height);
+ SetFilterStack();
+ nvgStroke(*m_nvg);
+ }
+
+ void Context::PlayPath2D(const NativeCanvasPath2D* path)
+ {
+ nvgBeginPath(*m_nvg);
+ for (const auto& command : *path)
+ {
+ const auto args = command.args;
+ switch (command.type)
+ {
+ case P2D_CLOSE:
+ nvgClosePath(*m_nvg);
+ break;
+ case P2D_MOVETO:
+ nvgMoveTo(*m_nvg, args.moveTo.x, args.moveTo.y);
+ break;
+ case P2D_LINETO:
+ nvgLineTo(*m_nvg, args.lineTo.x, args.lineTo.y);
+ break;
+ case P2D_BEZIERTO:
+ nvgBezierTo(*m_nvg, args.bezierTo.cp1x, args.bezierTo.cp1y,
+ args.bezierTo.cp2x, args.bezierTo.cp2y,
+ args.bezierTo.x, args.bezierTo.y);
+ break;
+ case P2D_QUADTO:
+ nvgQuadTo(*m_nvg, args.quadTo.cpx, args.quadTo.cpy,
+ args.quadTo.x, args.quadTo.y);
+ break;
+ case P2D_ARC:
+ nvgArc(*m_nvg, args.arc.x, args.arc.y, args.arc.radius,
+ args.arc.startAngle, args.arc.endAngle,
+ args.arc.counterclockwise ? NVG_CCW : NVG_CW);
+ break;
+ case P2D_ARCTO:
+ nvgArcTo(*m_nvg, args.arcTo.x1, args.arcTo.y1,
+ args.arcTo.x2, args.arcTo.y2,
+ args.arcTo.radius);
+ break;
+ case P2D_ELLIPSE:
+ // TODO: handle clockwise for nvgElipse (args.ellipse.counterclockwise)
+ nvgEllipse(*m_nvg, args.ellipse.x, args.ellipse.y,
+ args.ellipse.radiusX, args.ellipse.radiusY);
+ break;
+ case P2D_RECT:
+ nvgRect(*m_nvg, args.rect.x, args.rect.y,
+ args.rect.width, args.rect.height);
+ break;
+ case P2D_ROUNDRECT:
+ nvgRoundedRect(*m_nvg, args.roundRect.x, args.roundRect.y,
+ args.roundRect.width, args.roundRect.height,
+ args.roundRect.radii);
+ break;
+ case P2D_ROUNDRECTVARYING:
+ nvgRoundedRectVarying(*m_nvg, args.roundRectVarying.x, args.roundRectVarying.y,
+ args.roundRectVarying.width, args.roundRectVarying.height,
+ args.roundRectVarying.topLeft, args.roundRectVarying.topRight,
+ args.roundRectVarying.bottomRight, args.roundRectVarying.bottomLeft);
+ break;
+ case P2D_ROUNDRECTELLIPTIC:
+ nvgRoundedRectElliptic(*m_nvg, args.roundRectElliptic.x, args.roundRectElliptic.y,
+ args.roundRectElliptic.width, args.roundRectElliptic.height,
+ args.roundRectElliptic.topLeftX, args.roundRectElliptic.topLeftY,
+ args.roundRectElliptic.topRightX, args.roundRectElliptic.topRightY,
+ args.roundRectElliptic.bottomRightX, args.roundRectElliptic.bottomRightY,
+ args.roundRectElliptic.bottomLeftX, args.roundRectElliptic.bottomLeftY);
+ break;
+ case P2D_TRANSFORM:
+ nvgTransform(*m_nvg,
+ args.transform.a, args.transform.b, args.transform.c,
+ args.transform.d, args.transform.e, args.transform.f);
+ break;
+ default:
+ break;
+ }
+ }
}
- void Context::Stroke(const Napi::CallbackInfo&)
+ void Context::Stroke(const Napi::CallbackInfo& info)
{
- nvgStroke(m_nvg);
+ // draw Path2D if exists
+ const NativeCanvasPath2D* path = info.Length() == 1 ? NativeCanvasPath2D::Unwrap(info[0].As()) : nullptr;
+ if (path != nullptr)
+ {
+ PlayPath2D(path);
+ }
+
+ SetFilterStack();
+ nvgStroke(*m_nvg);
}
void Context::MoveTo(const Napi::CallbackInfo& info)
@@ -314,7 +539,7 @@ namespace Babylon::Polyfills::Internal
const auto x = info[0].As().FloatValue();
const auto y = info[1].As().FloatValue();
- nvgMoveTo(m_nvg, x, y);
+ nvgMoveTo(*m_nvg, x, y);
}
void Context::LineTo(const Napi::CallbackInfo& info)
@@ -322,7 +547,7 @@ namespace Babylon::Polyfills::Internal
const auto x = info[0].As().FloatValue();
const auto y = info[1].As().FloatValue();
- nvgLineTo(m_nvg, x, y);
+ nvgLineTo(*m_nvg, x, y);
}
void Context::QuadraticCurveTo(const Napi::CallbackInfo& info)
@@ -332,7 +557,7 @@ namespace Babylon::Polyfills::Internal
const auto x = info[2].As().FloatValue();
const auto y = info[3].As().FloatValue();
- nvgBezierTo(m_nvg, cx, cy, cx, cy, x, y);
+ nvgBezierTo(*m_nvg, cx, cy, cx, cy, x, y);
}
Napi::Value Context::MeasureText(const Napi::CallbackInfo& info)
@@ -341,44 +566,94 @@ namespace Babylon::Polyfills::Internal
return MeasureText::CreateInstance(info.Env(), this, text);
}
+ bool Context::SetFontFaceId()
+ {
+ if (m_fonts.empty())
+ {
+ return false;
+ }
+ else if (m_currentFontId >= 0)
+ {
+ nvgFontFaceId(*m_nvg, m_currentFontId);
+ }
+ else
+ {
+ nvgFontFaceId(*m_nvg, m_fonts.begin()->second);
+ }
+ return true;
+ }
+
void Context::FillText(const Napi::CallbackInfo& info)
{
- const std::string text = info[0].As().Utf8Value();
+ std::string text = info[0].As().Utf8Value();
auto x = info[1].As().FloatValue();
auto y = info[2].As().FloatValue();
- if (!m_fonts.empty())
+ // TODO: support ligatures, etc.
+ if (m_direction.compare("rtl") == 0) {
+ std::reverse(text.begin(), text.end());
+ }
+
+ if (SetFontFaceId())
{
- if (m_currentFontId >= 0)
- {
- nvgFontFaceId(m_nvg, m_currentFontId);
- }
- else
+ BindFillStyle(info, 0.f, 0.f, x, y);
+
+ if (m_filter.length())
{
- nvgFontFaceId(m_nvg, m_fonts.begin()->second);
+ nanovg_filterstack filterStack;
+ filterStack.ParseString(m_filter);
+ nvgFilterStack(*m_nvg, filterStack); // sets filterStack on nanovg
}
- nvgText(m_nvg, x, y, text.c_str(), nullptr);
+ nvgText(*m_nvg, x, y, text.c_str(), nullptr);
}
}
void Context::Flush(const Napi::CallbackInfo&)
{
- m_canvas->UpdateRenderTarget();
+ bool needClear = m_canvas->UpdateRenderTarget();
Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer();
auto updateToken{m_update.GetUpdateToken()};
bgfx::Encoder* encoder = updateToken.GetEncoder();
frameBuffer.Bind(*encoder);
+ if (needClear)
+ {
+ frameBuffer.Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0);
+ }
frameBuffer.SetViewPort(*encoder, 0.f, 0.f, 1.f, 1.f);
const auto width = m_canvas->GetWidth();
const auto height = m_canvas->GetHeight();
- nvgBeginFrame(m_nvg, float(width), float(height), 1.0f);
- nvgSetFrameBufferAndEncoder(m_nvg, frameBuffer, encoder);
- nvgEndFrame(m_nvg);
+ for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers())
+ {
+ // sanity check no buffers should have been acquired yet
+ assert(buffer.isAvailable == true);
+ }
+ std::function acquire = [this, encoder]() -> Babylon::Graphics::FrameBuffer* {
+ Babylon::Graphics::FrameBuffer *frameBuffer = this->m_canvas->m_frameBufferPool.Acquire();
+ frameBuffer->Bind(*encoder);
+ return frameBuffer;
+ };
+ std::function release = [this, encoder](Babylon::Graphics::FrameBuffer* frameBuffer) -> void {
+ // clear framebuffer when released
+ frameBuffer->Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0);
+ this->m_canvas->m_frameBufferPool.Release(frameBuffer);
+ frameBuffer->Unbind(*encoder);
+ };
+
+ nvgBeginFrame(*m_nvg, float(width), float(height), 1.0f);
+ nvgSetFrameBufferAndEncoder(*m_nvg, frameBuffer, encoder);
+ nvgSetFrameBufferPool(*m_nvg, { acquire, release });
+ nvgEndFrame(*m_nvg);
frameBuffer.Unbind(*encoder);
+
+ for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers())
+ {
+ // sanity check no unreleased buffers
+ assert(buffer.isAvailable == true);
+ }
}
void Context::PutImageData(const Napi::CallbackInfo&)
@@ -394,7 +669,7 @@ namespace Babylon::Polyfills::Internal
const auto startAngle = static_cast(info[3].As().DoubleValue());
const auto endAngle = static_cast(info[4].As().DoubleValue());
const NVGwinding winding = (info.Length() == 6 && info[5].As()) ? NVGwinding::NVG_CCW : NVGwinding::NVG_CW;
- nvgArc(m_nvg, x, y, radius, startAngle, endAngle, winding);
+ nvgArc(*m_nvg, x, y, radius, startAngle, endAngle, winding);
}
void Context::DrawImage(const Napi::CallbackInfo& info)
@@ -405,7 +680,7 @@ namespace Babylon::Polyfills::Internal
const auto nvgImageIter = m_nvgImageIndices.find(canvasImage);
if (nvgImageIter == m_nvgImageIndices.end())
{
- imageIndex = canvasImage->CreateNVGImageForContext(m_nvg);
+ imageIndex = canvasImage->CreateNVGImageForContext(*m_nvg);
m_nvgImageIndices.try_emplace(canvasImage, imageIndex);
}
else
@@ -421,16 +696,17 @@ namespace Babylon::Polyfills::Internal
const auto width = static_cast(canvasImage->GetWidth());
const auto height = static_cast(canvasImage->GetHeight());
- NVGpaint imagePaint = nvgImagePattern(m_nvg, 0.f, 0.f, width, height, 0.f, imageIndex, 1.f);
+ NVGpaint imagePaint = nvgImagePattern(*m_nvg, 0.f, 0.f, width, height, 0.f, imageIndex, 1.f);
if (!m_isClipped)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
- nvgRect(m_nvg, dx, dy, width, height);
- nvgFillPaint(m_nvg, imagePaint);
- nvgFill(m_nvg);
+ nvgRect(*m_nvg, dx, dy, width, height);
+ nvgFillPaint(*m_nvg, imagePaint);
+ SetFilterStack();
+ nvgFill(*m_nvg);
}
else if (info.Length() == 5)
{
@@ -439,16 +715,17 @@ namespace Babylon::Polyfills::Internal
const auto dWidth = static_cast(info[3].As().Uint32Value());
const auto dHeight = static_cast(info[4].As().Uint32Value());
- NVGpaint imagePaint = nvgImagePattern(m_nvg, dx, dy, dWidth, dHeight, 0.f, imageIndex, 1.f);
+ NVGpaint imagePaint = nvgImagePattern(*m_nvg, dx, dy, dWidth, dHeight, 0.f, imageIndex, 1.f);
if (!m_isClipped)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
- nvgRect(m_nvg, dx, dy, dWidth, dHeight);
- nvgFillPaint(m_nvg, imagePaint);
- nvgFill(m_nvg);
+ nvgRect(*m_nvg, dx, dy, dWidth, dHeight);
+ nvgFillPaint(*m_nvg, imagePaint);
+ SetFilterStack();
+ nvgFill(*m_nvg);
}
else if (info.Length() == 9)
{
@@ -463,16 +740,17 @@ namespace Babylon::Polyfills::Internal
const auto width = static_cast(canvasImage->GetWidth());
const auto height = static_cast(canvasImage->GetHeight());
- NVGpaint imagePaint = nvgImagePattern(m_nvg, dx, dy, dWidth, dHeight, 0.f, imageIndex, 1.f);
+ NVGpaint imagePaint = nvgImagePattern(*m_nvg, dx, dy, dWidth, dHeight, 0.f, imageIndex, 1.f);
if (!m_isClipped)
{
- nvgBeginPath(m_nvg);
+ nvgBeginPath(*m_nvg);
}
- nvgRect(m_nvg, dx, dy, dWidth, dHeight);
- nvgFillPaint(m_nvg, imagePaint);
- nvgFill(m_nvg);
+ nvgRect(*m_nvg, dx, dy, dWidth, dHeight);
+ nvgFillPaint(*m_nvg, imagePaint);
+ SetFilterStack();
+ nvgFill(*m_nvg);
}
else
{
@@ -498,42 +776,170 @@ namespace Babylon::Polyfills::Internal
void Context::StrokeText(const Napi::CallbackInfo& info)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ std::string text = info[0].As().Utf8Value();
+ auto x = info[1].As().FloatValue();
+ auto y = info[2].As().FloatValue();
+
+ // TODO: support ligatures, etc.
+ if (m_direction.compare("rtl") == 0) {
+ std::reverse(text.begin(), text.end());
+ }
+
+ if (SetFontFaceId())
+ {
+ nvgStrokeText(*m_nvg, x, y, text.c_str(), nullptr);
+ }
}
Napi::Value Context::CreateLinearGradient(const Napi::CallbackInfo& info)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ const auto x0 = info[0].As().FloatValue();
+ const auto y0 = info[1].As().FloatValue();
+ const auto x1 = info[2].As().FloatValue();
+ const auto y1 = info[3].As().FloatValue();
+
+ auto gradient = CanvasGradient::CreateLinear(info.Env(), m_nvg, x0, y0, x1, y1);
+ return gradient;
+ }
+
+ Napi::Value Context::CreateRadialGradient(const Napi::CallbackInfo& info)
+ {
+ const auto x0 = info[0].As().FloatValue();
+ const auto y0 = info[1].As().FloatValue();
+ const auto r0 = info[2].As().FloatValue();
+ const auto x1 = info[3].As().FloatValue();
+ const auto y1 = info[4].As().FloatValue();
+ const auto r1 = info[5].As().FloatValue();
+
+ auto gradient = CanvasGradient::CreateRadial(info.Env(), m_nvg, x0, y0, r0, x1, y1, r1);
+ return gradient;
+ }
+
+ Napi::Value Context::GetTransform(const Napi::CallbackInfo&)
+ {
+ float xform[6];
+ nvgCurrentTransform(*m_nvg, xform);
+
+ // set DOMMatrix properties
+ Napi::Object obj = Napi::Object::New(Env());
+ obj.Set("a", xform[0]);
+ obj.Set("b", xform[1]);
+ obj.Set("c", xform[2]);
+ obj.Set("d", xform[3]);
+ obj.Set("e", xform[4]);
+ obj.Set("f", xform[5]);
+ obj.Set("m11", xform[0]);
+ obj.Set("m12", xform[1]);
+ obj.Set("m13", 0);
+ obj.Set("m14", 0);
+ obj.Set("m21", xform[2]);
+ obj.Set("m22", xform[3]);
+ obj.Set("m23", 0);
+ obj.Set("m24", 0);
+ obj.Set("m31", 0);
+ obj.Set("m32", 0);
+ obj.Set("m33", 1);
+ obj.Set("m34", 0);
+ obj.Set("m41", xform[4]);
+ obj.Set("m42", xform[5]);
+ obj.Set("m43", 0);
+ obj.Set("m44", 1);
+ obj.Set("is2D", true);
+ obj.Set("isIdentity", false);
+ return obj;
}
void Context::SetTransform(const Napi::CallbackInfo& info)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ const auto a = info[0].As().FloatValue();
+ const auto b = info[1].As().FloatValue();
+ const auto c = info[2].As().FloatValue();
+ const auto d = info[3].As().FloatValue();
+ const auto e = info[4].As().FloatValue();
+ const auto f = info[5].As().FloatValue();
+ nvgResetTransform(*m_nvg);
+ nvgTransform(*m_nvg, a, b, c, d, e, f);
+ }
+
+ void Context::Transform(const Napi::CallbackInfo& info)
+ {
+ const auto a = info[0].As().FloatValue();
+ const auto b = info[1].As().FloatValue();
+ const auto c = info[2].As().FloatValue();
+ const auto d = info[3].As().FloatValue();
+ const auto e = info[4].As().FloatValue();
+ const auto f = info[5].As().FloatValue();
+ nvgTransform(*m_nvg, a, b, c, d, e, f);
+ }
+
+ Napi::Value Context::GetLineCap(const Napi::CallbackInfo& info)
+ {
+ return Napi::Value::From(Env(), m_lineCap);
+ }
+
+ void Context::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value)
+ {
+ m_lineCap = value.As().Utf8Value();
+ const auto lineCap = StringToLineCap(info.Env(), m_lineCap);
+ nvgLineCap(*m_nvg, lineCap);
}
Napi::Value Context::GetLineJoin(const Napi::CallbackInfo& info)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ return Napi::Value::From(Env(), m_lineJoin);
}
void Context::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ m_lineJoin = value.As().Utf8Value();
+ const auto lineJoin = StringToLineJoin(info.Env(), m_lineJoin);
+ nvgLineJoin(*m_nvg, lineJoin);
}
Napi::Value Context::GetMiterLimit(const Napi::CallbackInfo& info)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ return Napi::Value::From(Env(), m_miterLimit);
}
void Context::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value)
{
- throw Napi::Error::New(info.Env(), "not implemented");
+ m_miterLimit = value.As().FloatValue();
+ nvgMiterLimit(*m_nvg, m_miterLimit);
+ }
+
+ Napi::Value Context::GetFilter(const Napi::CallbackInfo& info)
+ {
+ return Napi::Value::From(Env(), m_filter);
+ }
+
+ void Context::SetFilter(const Napi::CallbackInfo& info, const Napi::Value& value)
+ {
+ std::string filterString = value.As().Utf8Value();
+ // Keep existing filter if the new one is invalid
+ if (nanovg_filterstack::ValidString(filterString))
+ {
+ m_filter = filterString;
+ }
+ }
+
+ Napi::Value Context::GetDirection(const Napi::CallbackInfo& info)
+ {
+ return Napi::Value::From(Env(), m_direction);
+ }
+
+ void Context::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value)
+ {
+ std::string direction = value.As().Utf8Value();
+ const bool valid = !(direction.compare("ltr") && direction.compare("rtl"));
+ if (valid)
+ {
+ m_direction = direction;
+ }
}
Napi::Value Context::GetFont(const Napi::CallbackInfo& info)
{
- return Napi::Value::From(Env(), m_font);
+ return Napi::Value::From(Env(), static_cast(m_font));
}
void Context::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value)
@@ -543,42 +949,52 @@ namespace Babylon::Polyfills::Internal
throw Napi::Error::New(info.Env(), "invalid argument");
}
- const std::string fontOptions = value.ToString();
+ auto font = Font::Parse(value.ToString());
+ if (!font)
+ {
+ return;
+ }
- // Default font id, and font size values.
- // TODO: Determine better way of signaling to user that font specified is invalid.
- m_currentFontId = -1;
- float fontSize{16.f};
+ nvgFontSize(*m_nvg, font->Size);
+ if (m_fonts.find(font->Family) == m_fonts.end())
+ {
+ // TODO: handle finding font face for a specific weight and style
+ m_currentFontId = -1;
+ }
+ else
+ {
+ m_currentFontId = m_fonts.at(font->Family);
+ }
- // Regex to parse font styling information. For now we are only capturing font size (capture group 3) and font family name (capture group 4).
- static const std::regex fontStyleRegex("([[a-zA-Z]+\\s+)*((\\d+(\\.\\d+)?)px\\s+)?(\\w+)");
- std::smatch fontStyleMatch;
+ m_font = std::move(*font);
+ }
- // Perform the actual regex_match.
- if (std::regex_match(fontOptions, fontStyleMatch, fontStyleRegex))
- {
- // Check if font size was specified.
- if (fontStyleMatch[3].matched)
- {
- fontSize = std::stof(fontStyleMatch[3]);
- }
+ Napi::Value Context::GetLetterSpacing(const Napi::CallbackInfo& info)
+ {
+ std::string letterSpacingStr = std::to_string(m_letterSpacing);
+ letterSpacingStr.erase(letterSpacingStr.find_last_not_of('0') + 1, std::string::npos);
+ letterSpacingStr.erase(letterSpacingStr.find_last_not_of('.') + 1, std::string::npos);
+ return Napi::Value::From(Env(), letterSpacingStr + "px");
+ }
- // Check if the specified font family name is valid, and if so assign the current font id.
- if (m_fonts.find(fontStyleMatch[4]) != m_fonts.end())
- {
- m_currentFontId = m_fonts.at(fontStyleMatch[4]);
- m_font = fontOptions;
- }
- }
+ void Context::SetLetterSpacing(const Napi::CallbackInfo& info, const Napi::Value& value)
+ {
+ const std::string letterSpacingOption = value.ToString();
- // Set font size on the current context.
- nvgFontSize(m_nvg, fontSize);
+ // regex the letter spacing string
+ static const std::regex letterSpacingRegex("(\\d+(\\.\\d+)?)px");
+ std::smatch letterSpacingMatch;
+ if (std::regex_match(letterSpacingOption, letterSpacingMatch, letterSpacingRegex))
+ {
+ m_letterSpacing = std::stof(letterSpacingMatch[1]);
+ }
+ nvgTextLetterSpacing(*m_nvg, m_letterSpacing);
}
void Context::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value)
{
const float alpha = value.As().FloatValue();
- nvgGlobalAlpha(m_nvg, alpha);
+ nvgGlobalAlpha(*m_nvg, alpha);
}
Napi::Value Context::GetShadowColor(const Napi::CallbackInfo& info)
diff --git a/Polyfills/Canvas/Source/Context.h b/Polyfills/Canvas/Source/Context.h
index 2d37a2241..e4472964e 100644
--- a/Polyfills/Canvas/Source/Context.h
+++ b/Polyfills/Canvas/Source/Context.h
@@ -4,11 +4,16 @@
#include
#include
#include "Image.h"
+#include "Path2D.h"
+#include "Font.h"
+#include "nanovg/nanovg_filterstack.h"
struct NVGcontext;
namespace Babylon::Polyfills::Internal
{
+ class CanvasGradient;
+
class Context final : public Napi::ObjectWrap, Polyfills::Canvas::Impl::MonitoredResource
{
public:
@@ -18,7 +23,7 @@ namespace Babylon::Polyfills::Internal
explicit Context(const Napi::CallbackInfo& info);
virtual ~Context();
- NVGcontext* GetNVGContext() const { return m_nvg; }
+ NVGcontext* GetNVGContext() const { return *m_nvg.get(); }
private:
void FillRect(const Napi::CallbackInfo&);
@@ -35,6 +40,7 @@ namespace Babylon::Polyfills::Internal
void ClosePath(const Napi::CallbackInfo&);
void Clip(const Napi::CallbackInfo&);
void Rect(const Napi::CallbackInfo&);
+ void RoundRect(const Napi::CallbackInfo&);
void StrokeRect(const Napi::CallbackInfo&);
void Stroke(const Napi::CallbackInfo&);
void MoveTo(const Napi::CallbackInfo&);
@@ -46,7 +52,10 @@ namespace Babylon::Polyfills::Internal
void SetLineDash(const Napi::CallbackInfo&);
void StrokeText(const Napi::CallbackInfo&);
Napi::Value CreateLinearGradient(const Napi::CallbackInfo&);
+ Napi::Value CreateRadialGradient(const Napi::CallbackInfo&);
+ Napi::Value GetTransform(const Napi::CallbackInfo&);
void SetTransform(const Napi::CallbackInfo&);
+ void Transform(const Napi::CallbackInfo&);
void QuadraticCurveTo(const Napi::CallbackInfo&);
Napi::Value GetFillStyle(const Napi::CallbackInfo&);
void SetFillStyle(const Napi::CallbackInfo&, const Napi::Value& value);
@@ -54,12 +63,20 @@ namespace Babylon::Polyfills::Internal
void SetStrokeStyle(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetLineWidth(const Napi::CallbackInfo&);
void SetLineWidth(const Napi::CallbackInfo&, const Napi::Value& value);
+ Napi::Value GetLineCap(const Napi::CallbackInfo&);
+ void SetLineCap(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetLineJoin(const Napi::CallbackInfo&);
void SetLineJoin(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetMiterLimit(const Napi::CallbackInfo&);
void SetMiterLimit(const Napi::CallbackInfo&, const Napi::Value& value);
+ Napi::Value GetFilter(const Napi::CallbackInfo& info);
+ void SetFilter(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetDirection(const Napi::CallbackInfo&);
+ void SetDirection(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetFont(const Napi::CallbackInfo&);
void SetFont(const Napi::CallbackInfo&, const Napi::Value& value);
+ Napi::Value GetLetterSpacing(const Napi::CallbackInfo&);
+ void SetLetterSpacing(const Napi::CallbackInfo&, const Napi::Value& value);
void SetGlobalAlpha(const Napi::CallbackInfo&, const Napi::Value& value);
Napi::Value GetShadowColor(const Napi::CallbackInfo&);
void SetShadowColor(const Napi::CallbackInfo&, const Napi::Value& value);
@@ -71,16 +88,23 @@ namespace Babylon::Polyfills::Internal
void SetShadowOffsetY(const Napi::CallbackInfo&, const Napi::Value& value);
void Dispose(const Napi::CallbackInfo&);
void Dispose();
+ bool SetFontFaceId();
void Flush(const Napi::CallbackInfo&);
NativeCanvas* m_canvas;
- NVGcontext* m_nvg;
+ std::shared_ptr m_nvg;
- std::string m_font{};
- std::string m_fillStyle{};
+ Font m_font;
+ std::variant m_fillStyle{};
std::string m_strokeStyle{};
+ std::string m_lineCap{}; // 'butt', 'round', 'square'
+ std::string m_lineJoin{}; // 'round', 'bevel', 'miter'
+ std::string m_filter{};
+ std::string m_direction{"ltr"}; // 'ltr', 'rtl'
+ float m_miterLimit{0.f};
float m_lineWidth{0.f};
float m_globalAlpha{1.f};
+ float m_letterSpacing{0.f};
std::map m_fonts;
int m_currentFontId{-1};
@@ -99,8 +123,10 @@ namespace Babylon::Polyfills::Internal
JsRuntimeScheduler m_runtimeScheduler;
std::unordered_map m_nvgImageIndices;
-
+ void BindFillStyle(const Napi::CallbackInfo& info, float left, float top, float width, float height);
void FlushGraphicResources() override;
+ void PlayPath2D(const NativeCanvasPath2D* path);
+ void SetFilterStack();
friend class Canvas;
};
diff --git a/Polyfills/Canvas/Source/Font.cpp b/Polyfills/Canvas/Source/Font.cpp
new file mode 100644
index 000000000..6c8f1ff9f
--- /dev/null
+++ b/Polyfills/Canvas/Source/Font.cpp
@@ -0,0 +1,97 @@
+#include
+#include
+
+#include "Font.h"
+
+namespace
+{
+ auto STYLE_REGEX = std::regex(R"(^\s*(normal|italic)\s)");
+ auto WEIGHT_REGEX = std::regex(R"(^\s*(normal|bold|\d+)\s)");
+ auto SIZE_REGEX = std::regex(R"(^\s*((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)px\s)");
+ auto FAMILY_IDENT_REGEX = std::regex(R"(^\s*((?:[\w-]|\\.)+))");
+ auto FAMILY_STRING_REGEX = std::regex(R"(^\s*(["'])((?:[^\\]|\\.)*?)\1)");
+}
+
+namespace Babylon::Polyfills::Internal
+{
+ std::optional Font::Parse(const std::string& fontString)
+ {
+ Font font;
+ auto begin = fontString.cbegin();
+ auto end = fontString.cend();
+ std::smatch match;
+
+ // The style and weight can be in any order
+ bool foundStyle = false;
+ bool foundWeight = false;
+ while (!foundStyle || !foundWeight)
+ {
+ if (!foundStyle && std::regex_search(begin, end, match, STYLE_REGEX))
+ {
+ begin = match[0].second;
+ foundStyle = true;
+ if (match[1] == "italic")
+ {
+ font.Style = FontStyle::Italic;
+ }
+ }
+ else if (!foundWeight && std::regex_search(begin, end, match, WEIGHT_REGEX))
+ {
+ begin = match[0].second;
+ foundWeight = true;
+ if (match[1] == "bold")
+ {
+ font.Weight = BOLD_WEIGHT;
+ }
+ else
+ {
+ font.Weight = std::stoi(match[1]);
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (!std::regex_search(begin, end, match, SIZE_REGEX))
+ {
+ return std::nullopt;
+ }
+ begin = match[0].second;
+ font.Size = std::stof(match[1]);
+
+ if (std::regex_search(begin, end, match, FAMILY_IDENT_REGEX))
+ {
+ font.Family = match[1];
+ }
+ else if (std::regex_search(begin, end, match, FAMILY_STRING_REGEX))
+ {
+ // The first capture group is used for the quotation mark (" or ')
+ font.Family = match[2];
+ }
+ else
+ {
+ return std::nullopt;
+ }
+
+ return font;
+ }
+
+ Font::operator std::string() const
+ {
+ std::ostringstream stream;
+ if (Style == FontStyle::Italic)
+ {
+ stream << "italic ";
+ }
+
+ if (Weight != NORMAL_WEIGHT)
+ {
+ stream << Weight << " ";
+ }
+
+ stream << Size << "px \"" << Family << "\"";
+ return stream.str();
+ }
+}
diff --git a/Polyfills/Canvas/Source/Font.h b/Polyfills/Canvas/Source/Font.h
new file mode 100644
index 000000000..c5ca4069d
--- /dev/null
+++ b/Polyfills/Canvas/Source/Font.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ enum class FontStyle
+ {
+ Normal,
+ Italic,
+ };
+
+ struct Font
+ {
+ private:
+ static constexpr int NORMAL_WEIGHT = 400;
+ static constexpr int BOLD_WEIGHT = 700;
+
+ public:
+ FontStyle Style = FontStyle::Normal;
+ int Weight = NORMAL_WEIGHT;
+ float Size = 10;
+ std::string Family = "sans-serif";
+
+ operator std::string() const;
+
+ static std::optional Parse(const std::string& fontString);
+ };
+}
diff --git a/Polyfills/Canvas/Source/FrameBufferPool.cpp b/Polyfills/Canvas/Source/FrameBufferPool.cpp
new file mode 100644
index 000000000..ea7b415ff
--- /dev/null
+++ b/Polyfills/Canvas/Source/FrameBufferPool.cpp
@@ -0,0 +1,122 @@
+#include
+#include
+#include
+
+#include
+#include
+#include "FrameBufferPool.h"
+
+namespace Babylon::Polyfills
+{
+ const std::vector& FrameBufferPool::GetPoolBuffers()
+ {
+ return mPoolBuffers;
+ }
+
+ // sets dimension of framebuffers, can only be set if no buffers in pool
+ void FrameBufferPool::SetDimensions(int width, int height)
+ {
+ assert(width > 0 && height > 0);
+ // TODO: support multiple framebuffer dimensions
+ if (mPoolBuffers.size() > 0)
+ {
+ throw std::runtime_error("Cannot set dimensions. FrameBufferPool already has buffers.");
+ }
+
+ this->m_width = width;
+ this->m_height = height;
+ }
+
+ // sets graphics context to be used for creating framebuffers
+ void FrameBufferPool::SetGraphicsContext(Graphics::DeviceContext* graphicsContext)
+ {
+ m_graphicsContext = graphicsContext;
+ }
+
+ void FrameBufferPool::Add(int nBuffers)
+ {
+ if (m_graphicsContext == nullptr)
+ {
+ throw std::runtime_error("Cannot add framebuffer to pool. Graphics context is not set.");
+ }
+
+ for (int i = 0; i < nBuffers; ++i)
+ {
+ bgfx::FrameBufferHandle TextBuffer{bgfx::kInvalidHandle};
+ Graphics::FrameBuffer* FrameBuffer;
+
+ // make sure render targets are filled with 0 : https://registry.khronos.org/webgl/specs/latest/1.0/#TEXIMAGE2D
+ bgfx::ReleaseFn releaseFn{[](void*, void* userData) {
+ bimg::imageFree(static_cast(userData));
+ }};
+
+ bimg::ImageContainer* image = bimg::imageAlloc(&Babylon::Graphics::DeviceContext::GetDefaultAllocator(), bimg::TextureFormat::RGBA8, m_width, m_height, 1 /*depth*/, 1, false /*cubeMap*/, false /*hasMips*/);
+ const bgfx::Memory* mem = bgfx::makeRef(image->m_data, image->m_size, releaseFn, image);
+ bx::memSet(image->m_data, 0, image->m_size);
+ // TODO: make sampler flags configurable
+ // border sampling will result in transparent edge artifacts for blur, but this behaviour is consistent with browser implementation
+ std::array textures{
+ bgfx::createTexture2D(m_width, m_height, false, 1, bgfx::TextureFormat::RGBA8, BGFX_TEXTURE_RT | BGFX_SAMPLER_U_BORDER | BGFX_SAMPLER_V_BORDER | BGFX_SAMPLER_BORDER_COLOR(0), mem),
+ bgfx::createTexture2D(m_width, m_height, false, 1, bgfx::TextureFormat::D24S8, BGFX_TEXTURE_RT | BGFX_SAMPLER_U_BORDER | BGFX_SAMPLER_V_BORDER | BGFX_SAMPLER_BORDER_COLOR(0))};
+
+ std::array attachments{};
+ for (size_t idx = 0; idx < attachments.size(); ++idx)
+ {
+ attachments[idx].init(textures[idx]);
+ }
+ TextBuffer = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), true);
+
+ FrameBuffer = new Graphics::FrameBuffer(*m_graphicsContext, TextBuffer, m_width, m_height, false, false, false);
+ m_available++;
+ mPoolBuffers.push_back({FrameBuffer, true});
+ }
+ }
+
+ void FrameBufferPool::Clear()
+ {
+ for (auto& buffer : mPoolBuffers)
+ {
+ if (buffer.frameBuffer)
+ {
+ buffer.frameBuffer->Dispose();
+ delete buffer.frameBuffer;
+ }
+ }
+ m_available = 0;
+ mPoolBuffers.clear();
+ }
+
+ Graphics::FrameBuffer* FrameBufferPool::Acquire()
+ {
+ // no buffers in pool, add one
+ if (m_available == 0)
+ {
+ Add(1);
+ }
+
+ for (auto& buffer : mPoolBuffers)
+ {
+ if (buffer.isAvailable)
+ {
+ buffer.isAvailable = false;
+ m_available--;
+ return buffer.frameBuffer;
+ }
+ }
+
+ throw std::runtime_error("No available frame buffer in pool.");
+ }
+
+ void FrameBufferPool::Release(Graphics::FrameBuffer* frameBuffer)
+ {
+ for (auto& buffer : mPoolBuffers)
+ {
+ if (buffer.frameBuffer == frameBuffer)
+ {
+ buffer.isAvailable = true;
+ m_available++;
+ return;
+ }
+ }
+ }
+}
diff --git a/Polyfills/Canvas/Source/FrameBufferPool.h b/Polyfills/Canvas/Source/FrameBufferPool.h
new file mode 100644
index 000000000..003dc88d7
--- /dev/null
+++ b/Polyfills/Canvas/Source/FrameBufferPool.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include
+#include
+
+namespace Babylon::Polyfills
+{
+ class FrameBufferPool final
+ {
+ public:
+ struct PoolBuffer
+ {
+ Graphics::FrameBuffer* frameBuffer;
+ bool isAvailable;
+ };
+ // acquire a frame buffer from the pool, graphics context must be set
+ Graphics::FrameBuffer* Acquire();
+ void Add(int nBuffers);
+ void Clear();
+ void Release(Graphics::FrameBuffer* frameBuffer);
+ void SetDimensions(int width, int height);
+ // sets graphics context to be used for creating framebuffers
+ void SetGraphicsContext(Graphics::DeviceContext *graphicsContext);
+ const std::vector& GetPoolBuffers();
+
+ private:
+ std::vector mPoolBuffers{};
+ Graphics::DeviceContext* m_graphicsContext;
+ int m_available{0};
+ int m_width{256};
+ int m_height{256};
+ };
+}
diff --git a/Polyfills/Canvas/Source/Gradient.cpp b/Polyfills/Canvas/Source/Gradient.cpp
new file mode 100644
index 000000000..fd5e9738a
--- /dev/null
+++ b/Polyfills/Canvas/Source/Gradient.cpp
@@ -0,0 +1,354 @@
+#include
+#include