| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include "testing_window.hpp" | |
| #if !defined(RIVE_CANVAS_2D) | |
| TestingWindow* TestingWindow::MakeCanvas2D() { return nullptr; } | |
| #else | |
| #include "utils/factory_utils.hpp" | |
| #include <string> | |
| #include <vector> | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/html5.h> | |
| #include <emscripten/val.h> | |
| namespace rive::gpu | |
| { | |
| EM_ASYNC_JS(void, testingWindowCanvas2dImportCanvasAdvanced, (), { | |
| // The import itself is async, and then we need to call the "default" | |
| // function to get the module, which is also async. | |
| // clang-format off | |
| if (globalThis.canvasAdvancedModule === undefined) | |
| { | |
| const result = await import("./canvas_advanced.mjs"); | |
| globalThis.canvasAdvancedModule = await result.default(); | |
| } | |
| // clang-format on | |
| }); | |
| // Resets the canvas and fills it with `color` (0xAARRGGBB). The fill has to | |
| // happen here rather than through the renderer: a drawPath composites over the | |
| // existing pixels, which cannot produce a transparent clear color. | |
| EM_JS(void, testingWindowCanvas2dResetCanvas, (uint32_t color), { | |
| var canvas = document["getElementById"]("canvas"); | |
| var ctx = canvas["getContext"]("2d"); | |
| ctx["reset"](); | |
| // reset() leaves the canvas transparent, so compositing the clear color | |
| // over it yields exactly that color, alpha included. | |
| ctx["fillStyle"] = "rgba(" + ((0x00ff0000 & color) >>> 16) + "," + | |
| ((0x0000ff00 & color) >>> 8) + "," + | |
| ((0x000000ff & color) >>> 0) + "," + | |
| ((0xff000000 & color) >>> 24) / 0xff + ")"; | |
| ctx["fillRect"](0, 0, canvas["width"], canvas["height"]); | |
| }); | |
| EM_JS(bool, | |
| testingWindowCanvas2dGetCanvasPixels, | |
| (uint8_t* outBuffer, int width, int height), | |
| { | |
| var canvas = document["getElementById"]("canvas"); | |
| var ctx = canvas["getContext"]("2d"); | |
| var imageData = | |
| ctx["getImageData"](0, 0, canvas.width, canvas.height); | |
| var pixelArray = imageData["data"]; | |
| if (pixelArray.length != width * height * 4) | |
| { | |
| return false; | |
| } | |
| // TestHarness::savePNG() flips vertically on the way out, since every | |
| // other backend fills this buffer bottom-up (the GL convention). | |
| // getImageData() hands back rows top-down, so reverse them here and | |
| // let the two cancel. | |
| var stride = width * 4; | |
| for (var y = 0; y < height; ++y) | |
| { | |
| Module['HEAPU8']['set']( | |
| pixelArray['subarray']((height - 1 - y) * stride, | |
| (height - y) * stride), | |
| outBuffer + y * stride); | |
| } | |
| return true; | |
| }); | |
| // Image decoding is asynchronous: renderer.js hands the bytes to an <img> | |
| // element through a blob URL and only fills in the result on its load event. | |
| // | |
| // Note this goes through Module.decodeImage rather than the more obvious | |
| // renderFactory.makeRenderImage(). The latter's onDecode callback closes over | |
| // renderer.js's loadContext, which is only non-null inside Module.load(), and | |
| // this harness draws images directly without ever loading a .riv file, so that | |
| // path would dereference null on the first decode. | |
| EM_ASYNC_JS(emscripten::EM_VAL, | |
| testingWindowCanvas2dDecodeImage, | |
| (const uint8_t* bytes, int size), | |
| { | |
| // Copy out of our heap up front just to be extra safe | |
| // (renderer.js's `decode` function also makes a Blob copy). | |
| var copy = Module['HEAPU8']['slice'](bytes, bytes + size); | |
| var image = await new Promise(function(resolve) { | |
| globalThis.canvasAdvancedModule["decodeImage"](copy, | |
| resolve); | |
| }); | |
| return Emval.toHandle(image); | |
| }); | |
| // Resolved on first use rather than at static-init time, since the module isn't | |
| // on globalThis until testingWindowCanvas2dImportCanvasAdvanced() has finished. | |
| static emscripten::val& canvasAdvancedModule() | |
| { | |
| static emscripten::val module = | |
| emscripten::val::global("canvasAdvancedModule"); | |
| return module; | |
| } | |
| // embind exposes C++ enums to JS as value objects, not plain numbers, and | |
| // renderer.js compares them by identity. This class maps the C++ int-based | |
| // enums to the objects stored in canvas_advanced's heap (which is different | |
| // from this wasm module's heap!). | |
| // | |
| // embind stores the reverse map on the enum's JS constructor as | |
| // `.values[rawValue]` (see _embind_register_enum_value in libembind.js), which | |
| // lets us build the table without naming every enumerator here. | |
| class JSEnum | |
| { | |
| public: | |
| // Must not be constructed until canvas_advanced.mjs has finished loading. | |
| explicit JSEnum(const char* enumName) | |
| { | |
| emscripten::val values = canvasAdvancedModule()[enumName]["values"]; | |
| assert(!values.isUndefined()); // Not an embind enum? | |
| // Index the table by raw value. Some of these enums are sparse, so the | |
| // gaps stay undefined and are caught by the assert in operator(). | |
| emscripten::val keys = | |
| emscripten::val::global("Object").call<emscripten::val>("keys", | |
| values); | |
| size_t keyCount = keys["length"].as<size_t>(); | |
| for (size_t i = 0; i < keyCount; i++) | |
| { | |
| int rawValue = std::stoi(keys[i].as<std::string>()); | |
| assert(rawValue >= 0); | |
| // We're storing the enum mapping in a vector rather than a sparse | |
| // data structure, like a map. If we ever try to register an enum | |
| // with really high values, it might indicate that this enum is | |
| // meant to be used as a bit-mask, and the vector will become huge. | |
| // If that happens, this assert will trigger and we can switch to | |
| // something more appropriate. | |
| assert(rawValue < 1024); | |
| if (static_cast<size_t>(rawValue) >= m_values.size()) | |
| { | |
| m_values.resize(rawValue + 1, emscripten::val::undefined()); | |
| } | |
| m_values[rawValue] = values[rawValue]; | |
| } | |
| } | |
| template <typename T> const emscripten::val& operator()(T value) const | |
| { | |
| size_t rawValue = static_cast<size_t>(value); | |
| assert(rawValue < m_values.size()); | |
| assert(!m_values[rawValue].isUndefined()); | |
| return m_values[rawValue]; | |
| } | |
| private: | |
| std::vector<emscripten::val> m_values; | |
| }; | |
| // The names here are the ones canvas_advanced registers in | |
| // EMSCRIPTEN_BINDINGS(RiveWASM_C2D); they are not the C++ type names. | |
| struct JSEnums | |
| { | |
| JSEnum blendMode{"BlendMode"}; | |
| JSEnum fillRule{"FillRule"}; | |
| JSEnum paintStyle{"RenderPaintStyle"}; | |
| JSEnum strokeCap{"StrokeCap"}; | |
| JSEnum strokeJoin{"StrokeJoin"}; | |
| }; | |
| // Built on first use, which must be after the import wait loop in | |
| // TestingWindowCanvas2D's constructor. | |
| static const JSEnums& jsEnums() | |
| { | |
| static JSEnums enums; | |
| return enums; | |
| } | |
| // The classes below let this module drive the canvas2d renderer that lives in | |
| // canvas_advanced.mjs. | |
| // | |
| // This harness and canvas_advanced are separately linked Emscripten modules: | |
| // two WebAssembly.Memory instances, two C++ runtimes, two embind type | |
| // registries. A pointer is only an offset into one module's linear memory, so | |
| // nothing that is or contains one -- a rive::Renderer*, an rcp<>, a vtable -- | |
| // can be handed across. JS objects can, since there is only one JavaScript | |
| // realm; that includes TypedArrays, which are ordinary JS objects that happen | |
| // to view a particular module's heap. | |
| // | |
| // So each class holds the emscripten::val of its canvas_advanced counterpart | |
| // and forwards to it, either directly for simple cases (e.g. | |
| // m_jsPath.call<void>("moveTo", x, y)), or through Canvas2DTestUtilities, where | |
| // that wrapper has logic worth exercising rather than duplicating. | |
| // | |
| // Ownership runs opposite to bindings_c2d.cpp: there the factory adopts the C++ | |
| // object out of the JS handle, whereas here the handle stays the sole owner, | |
| // and embind never reclaims raw pointer handles by itself. So each destructor | |
| // releases explicitly, via deleteLater() rather than delete() because | |
| // renderer.js captures paths and paints in deferred draw-list closures that | |
| // must outlive us until endFrame() flushes them. | |
| // | |
| // Every renderer object we see originates from JSRenderFactory, so a failed | |
| // downcast is a bug rather than a legitimate "some other subclass" case -- | |
| // hence lite_rtti_cast_or_assert<> rather than the silent LITE_RTTI_CAST_OR_* | |
| // macros. | |
| // Gradients have no JS-side object to wrap, so there is no handle for | |
| // makeLinearGradient() to return. Instead we hold the parameters and replay | |
| // them onto the paint when JSRenderPaint::shader() is called, mirroring | |
| // GradientShader in bindings_c2d.cpp. | |
| class JSGradientShader | |
| : public LITE_RTTI_OVERRIDE(RenderShader, JSGradientShader) | |
| { | |
| public: | |
| JSGradientShader(const ColorInt colors[], | |
| const float stops[], | |
| size_t count) : | |
| m_stops(stops, stops + count), m_colors(colors, colors + count) | |
| {} | |
| virtual void applyToPaint(const emscripten::val& jsPaint) const = 0; | |
| protected: | |
| void applyStopsToPaint(const emscripten::val& jsPaint) const | |
| { | |
| for (size_t i = 0; i < m_stops.size(); ++i) | |
| { | |
| jsPaint.call<void>("addStop", m_colors[i], m_stops[i]); | |
| } | |
| } | |
| private: | |
| std::vector<float> m_stops; | |
| std::vector<ColorInt> m_colors; | |
| }; | |
| class JSLinearGradientShader : public JSGradientShader | |
| { | |
| public: | |
| JSLinearGradientShader(const ColorInt colors[], | |
| const float stops[], | |
| size_t count, | |
| float sx, | |
| float sy, | |
| float ex, | |
| float ey) : | |
| JSGradientShader(colors, stops, count), | |
| m_startX(sx), | |
| m_startY(sy), | |
| m_endX(ex), | |
| m_endY(ey) | |
| {} | |
| void applyToPaint(const emscripten::val& jsPaint) const override | |
| { | |
| jsPaint.call<void>("linearGradient", | |
| m_startX, | |
| m_startY, | |
| m_endX, | |
| m_endY); | |
| applyStopsToPaint(jsPaint); | |
| } | |
| private: | |
| float m_startX; | |
| float m_startY; | |
| float m_endX; | |
| float m_endY; | |
| }; | |
| class JSRadialGradientShader : public JSGradientShader | |
| { | |
| public: | |
| JSRadialGradientShader(const ColorInt colors[], | |
| const float stops[], | |
| size_t count, | |
| float cx, | |
| float cy, | |
| float radius) : | |
| JSGradientShader(colors, stops, count), | |
| m_centerX(cx), | |
| m_centerY(cy), | |
| m_radius(radius) | |
| {} | |
| void applyToPaint(const emscripten::val& jsPaint) const override | |
| { | |
| // renderer.js wants the radius expressed as a second point rather than | |
| // a scalar. Matches RadialGradientShader::passToJS in bindings_c2d.cpp. | |
| jsPaint.call<void>("radialGradient", | |
| m_centerX, | |
| m_centerY, | |
| m_centerX + m_radius, | |
| m_centerY); | |
| applyStopsToPaint(jsPaint); | |
| } | |
| private: | |
| float m_centerX; | |
| float m_centerY; | |
| float m_radius; | |
| }; | |
| class JSRenderPath : public LITE_RTTI_OVERRIDE(RenderPath, JSRenderPath) | |
| { | |
| public: | |
| JSRenderPath(emscripten::val&& jsPath) : | |
| m_jsPath(std::forward<emscripten::val>(jsPath)) | |
| {} | |
| // Queued rather than deleted outright: renderer.js's _drawPath/_clipPath | |
| // capture the path in deferred draw-list closures, so it has to outlive us | |
| // until TestingWindowCanvas2D::endFrame() flushes them. See the comment on | |
| // flushPendingDeletes() there. | |
| ~JSRenderPath() override { m_jsPath.call<void>("deleteLater"); } | |
| void rewind() override { m_jsPath.call<void>("rewind"); } | |
| void fillRule(FillRule value) override | |
| { | |
| m_jsPath.call<void>("fillRule", jsEnums().fillRule(value)); | |
| } | |
| void moveTo(float x, float y) override | |
| { | |
| m_jsPath.call<void>("moveTo", x, y); | |
| } | |
| void lineTo(float x, float y) override | |
| { | |
| m_jsPath.call<void>("lineTo", x, y); | |
| } | |
| void cubicTo(float ox, float oy, float ix, float iy, float x, float y) | |
| override | |
| { | |
| m_jsPath.call<void>("cubicTo", ox, oy, ix, iy, x, y); | |
| } | |
| void close() override { m_jsPath.call<void>("close"); } | |
| void addRenderPath(const RenderPath* path, const Mat2D& transform) override | |
| { | |
| float xx = transform.xx(); | |
| float xy = transform.xy(); | |
| float yx = transform.yx(); | |
| float yy = transform.yy(); | |
| float tx = transform.tx(); | |
| float ty = transform.ty(); | |
| auto* jsPath = | |
| rive::lite_rtti_cast_or_assert<const JSRenderPath*>(path); | |
| m_jsPath.call<void>("addPath", jsPath->js(), xx, xy, yx, yy, tx, ty); | |
| } | |
| void addRawPath(const RawPath& path) override | |
| { | |
| const rive::Vec2D* pts = path.points().data(); | |
| for (auto v : path.verbs()) | |
| { | |
| switch ((rive::PathVerb)v) | |
| { | |
| case rive::PathVerb::move: | |
| move(*pts++); | |
| break; | |
| case rive::PathVerb::line: | |
| line(*pts++); | |
| break; | |
| case rive::PathVerb::cubic: | |
| cubic(pts[0], pts[1], pts[2]); | |
| pts += 3; | |
| break; | |
| case rive::PathVerb::close: | |
| close(); | |
| break; | |
| default: | |
| assert(false); // unexpected verb | |
| } | |
| } | |
| assert(pts - path.points().data() == path.points().size()); | |
| } | |
| emscripten::val& js() { return m_jsPath; } | |
| const emscripten::val& js() const { return m_jsPath; } | |
| private: | |
| emscripten::val m_jsPath; | |
| }; | |
| class JSRenderPaint : public LITE_RTTI_OVERRIDE(RenderPaint, JSRenderPaint) | |
| { | |
| public: | |
| JSRenderPaint(emscripten::val&& jsPaint) : | |
| m_jsPaint(std::forward<emscripten::val>(jsPaint)) | |
| {} | |
| ~JSRenderPaint() override { m_jsPaint.call<void>("deleteLater"); } | |
| void style(RenderPaintStyle style) override | |
| { | |
| m_jsPaint.call<void>("style", jsEnums().paintStyle(style)); | |
| } | |
| void color(ColorInt value) override | |
| { | |
| m_jsPaint.call<void>("color", value); | |
| } | |
| void thickness(float value) override | |
| { | |
| m_jsPaint.call<void>("thickness", value); | |
| } | |
| void join(StrokeJoin value) override | |
| { | |
| m_jsPaint.call<void>("join", jsEnums().strokeJoin(value)); | |
| } | |
| void cap(StrokeCap value) override | |
| { | |
| m_jsPaint.call<void>("cap", jsEnums().strokeCap(value)); | |
| } | |
| void feather(float value) override | |
| { | |
| // Not currently implemented (yet) | |
| } | |
| void blendMode(BlendMode value) override | |
| { | |
| m_jsPaint.call<void>("blendMode", jsEnums().blendMode(value)); | |
| } | |
| void shader(rcp<RenderShader> shader) override | |
| { | |
| if (shader == nullptr) | |
| { | |
| m_jsPaint.call<void>("clearGradient"); | |
| return; | |
| } | |
| rive::lite_rtti_cast_or_assert<JSGradientShader*>(shader.get()) | |
| ->applyToPaint(m_jsPaint); | |
| } | |
| void invalidateStroke() override {} | |
| emscripten::val& js() { return m_jsPaint; } | |
| const emscripten::val& js() const { return m_jsPaint; } | |
| private: | |
| emscripten::val m_jsPaint; | |
| }; | |
| class JSRenderImage : public LITE_RTTI_OVERRIDE(RenderImage, JSRenderImage) | |
| { | |
| public: | |
| JSRenderImage(emscripten::val&& jsImage) : | |
| m_jsImage(std::forward<emscripten::val>(jsImage)) | |
| { | |
| // On load, renderer.js calls size() on the RenderImage it created, | |
| // which sets the dimensions on the wrapper in canvas_advanced's heap. | |
| // Ours is a separate rive::RenderImage with its own m_Width/m_Height, | |
| // and rive core reads those, so copy them across. | |
| emscripten::val testUtils = | |
| canvasAdvancedModule()["Canvas2DTestUtilities"]; | |
| m_Width = testUtils.call<int>("imageWidth", m_jsImage); | |
| m_Height = testUtils.call<int>("imageHeight", m_jsImage); | |
| } | |
| ~JSRenderImage() override { m_jsImage.call<void>("deleteLater"); } | |
| emscripten::val& js() { return m_jsImage; } | |
| const emscripten::val& js() const { return m_jsImage; } | |
| private: | |
| emscripten::val m_jsImage; | |
| }; | |
| class JSRenderFactory : public rive::Factory | |
| { | |
| public: | |
| JSRenderFactory() : m_jsFactory(canvasAdvancedModule()["renderFactory"]) {} | |
| rcp<RenderBuffer> makeRenderBuffer(RenderBufferType type, | |
| RenderBufferFlags flags, | |
| size_t sizeInBytes) override | |
| { | |
| // These never cross the module boundary: rive core maps and fills them | |
| // here, and JSRenderer::drawImageMesh() copies the contents over when | |
| // it hands off. Must be a DataRenderBuffer for the LITE_RTTI casts | |
| // there to succeed. | |
| return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes); | |
| } | |
| rcp<RenderShader> makeLinearGradient(float sx, | |
| float sy, | |
| float ex, | |
| float ey, | |
| const ColorInt colors[], | |
| const float stops[], | |
| size_t count) override | |
| { | |
| return rcp<RenderShader>( | |
| new JSLinearGradientShader(colors, stops, count, sx, sy, ex, ey)); | |
| } | |
| rcp<RenderShader> makeRadialGradient(float cx, | |
| float cy, | |
| float radius, | |
| const ColorInt colors[], | |
| const float stops[], | |
| size_t count) override | |
| { | |
| return rcp<RenderShader>( | |
| new JSRadialGradientShader(colors, stops, count, cx, cy, radius)); | |
| } | |
| rcp<RenderPath> makeRenderPath(RawPath& rawPath, FillRule fillRule) override | |
| { | |
| rcp<RenderPath> renderPath = makeEmptyRenderPath(); | |
| renderPath->addRawPath(rawPath); | |
| renderPath->fillRule(fillRule); | |
| return renderPath; | |
| } | |
| rcp<RenderPath> makeEmptyRenderPath() override | |
| { | |
| return make_rcp<JSRenderPath>( | |
| m_jsFactory.call<emscripten::val>("makeRenderPath")); | |
| } | |
| rcp<RenderPaint> makeRenderPaint() override | |
| { | |
| return make_rcp<JSRenderPaint>( | |
| m_jsFactory.call<emscripten::val>("makeRenderPaint")); | |
| } | |
| rcp<RenderImage> decodeImage(Span<const uint8_t> bytes) override | |
| { | |
| if (bytes.empty()) | |
| { | |
| return nullptr; | |
| } | |
| // Blocks until the <img> load event fires. | |
| return make_rcp<JSRenderImage>(emscripten::val::take_ownership( | |
| testingWindowCanvas2dDecodeImage(bytes.data(), | |
| static_cast<int>(bytes.size())))); | |
| } | |
| private: | |
| emscripten::val m_jsFactory; | |
| }; | |
| class JSRenderer : public rive::Renderer | |
| { | |
| public: | |
| JSRenderer() : | |
| m_jsRenderer(canvasAdvancedModule().call<emscripten::val>( | |
| "makeRenderer", | |
| emscripten::val::global("document") | |
| .call<emscripten::val>("getElementById", | |
| std::string("canvas")))) | |
| {} | |
| // Queued, not deleted: renderer.js holds us in _pendingCanvasRenderers | |
| // until the draw list is flushed. | |
| ~JSRenderer() override { m_jsRenderer.call<void>("deleteLater"); } | |
| void beginFrame(bool clear) | |
| { | |
| m_jsRenderer.call<void>("beginFrame", clear); | |
| } | |
| void save() override { m_jsRenderer.call<void>("save"); } | |
| void restore() override { m_jsRenderer.call<void>("restore"); } | |
| void transform(const Mat2D& matrix) override | |
| { | |
| m_jsRenderer.call<void>("transform", | |
| matrix.xx(), | |
| matrix.xy(), | |
| matrix.yx(), | |
| matrix.yy(), | |
| matrix.tx(), | |
| matrix.ty()); | |
| } | |
| void drawPath(RenderPath* path, RenderPaint* paint) override | |
| { | |
| auto* jsPath = rive::lite_rtti_cast_or_assert<JSRenderPath*>(path); | |
| auto* jsPaint = rive::lite_rtti_cast_or_assert<JSRenderPaint*>(paint); | |
| m_jsRenderer.call<void>("_drawPath", jsPath->js(), jsPaint->js()); | |
| } | |
| void clipPath(RenderPath* path) override | |
| { | |
| auto* jsPath = rive::lite_rtti_cast_or_assert<JSRenderPath*>(path); | |
| m_jsRenderer.call<void>("_clipPath", jsPath->js()); | |
| } | |
| void drawImage(const RenderImage* image, | |
| ImageSampler sampler, | |
| BlendMode blendMode, | |
| float opacity) override | |
| { | |
| auto* jsImage = | |
| rive::lite_rtti_cast_or_assert<const JSRenderImage*>(image); | |
| m_jsRenderer.call<void>("_drawRiveImage", | |
| jsImage->js(), | |
| jsEnums().blendMode(blendMode), | |
| opacity); | |
| } | |
| void drawImageMesh(const RenderImage* image, | |
| ImageSampler sampler, | |
| rcp<RenderBuffer> vertices_f32, | |
| rcp<RenderBuffer> uvCoords_f32, | |
| rcp<RenderBuffer> indices_u16, | |
| uint32_t vertexCount, | |
| uint32_t indexCount, | |
| BlendMode blendMode, | |
| float opacity) override | |
| { | |
| auto* vtx = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>( | |
| vertices_f32.get()); | |
| auto* uv = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>( | |
| uvCoords_f32.get()); | |
| auto* indices = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>( | |
| indices_u16.get()); | |
| uint32_t f32Count = vertexCount * 2; | |
| assert(vtx->sizeInBytes() == f32Count * sizeof(float)); | |
| assert(uv->sizeInBytes() == f32Count * sizeof(float)); | |
| assert(indices->sizeInBytes() == indexCount * sizeof(uint16_t)); | |
| if (f32Count == 0 || indexCount == 0) | |
| { | |
| return; | |
| } | |
| // Unlike the rest of the shims, this one hands off to the C++ side of | |
| // canvas_advanced rather than to renderer.js, so that the real | |
| // RendererWrapper::drawImageMesh() runs -- it owns the mesh bounding | |
| // box computation and the atlas packing, and we'd otherwise have to | |
| // duplicate them here (and leave them untested). | |
| // | |
| // The typed_memory_views below are over our heap. A TypedArray is a | |
| // plain JS object, so canvas_advanced can read it even though it wraps | |
| // a different ArrayBuffer; it copies into its own DataRenderBuffers. | |
| // Nothing between here and the call allocates locally, so our memory | |
| // can't grow and detach them in the meantime. | |
| auto* jsImage = | |
| rive::lite_rtti_cast_or_assert<const JSRenderImage*>(image); | |
| canvasAdvancedModule()["Canvas2DTestUtilities"].call<void>( | |
| "drawImageMesh", | |
| m_jsRenderer, | |
| jsImage->js(), | |
| emscripten::val{ | |
| emscripten::typed_memory_view(f32Count, vtx->f32s())}, | |
| emscripten::val{ | |
| emscripten::typed_memory_view(f32Count, uv->f32s())}, | |
| emscripten::val{ | |
| emscripten::typed_memory_view(indexCount, indices->u16s())}, | |
| jsEnums().blendMode(blendMode), | |
| opacity); | |
| } | |
| void modulateOpacity(float opacity) override | |
| { | |
| m_jsRenderer.call<void>("modulateOpacity", opacity); | |
| } | |
| private: | |
| emscripten::val m_jsRenderer; | |
| }; | |
| class TestingWindowCanvas2D : public TestingWindow | |
| { | |
| public: | |
| TestingWindowCanvas2D() | |
| { | |
| testingWindowCanvas2dImportCanvasAdvanced(); | |
| int w, h; | |
| emscripten_get_canvas_element_size("#canvas", &w, &h); | |
| m_width = w; | |
| m_height = h; | |
| m_factory = std::make_unique<JSRenderFactory>(); | |
| } | |
| rive::Factory* factory() override { return m_factory.get(); } | |
| void resize(int width, int height) override | |
| { | |
| if (m_width != width || m_height != height) | |
| { | |
| TestingWindow::resize(width, height); | |
| emscripten_set_canvas_element_size("#canvas", width, height); | |
| } | |
| } | |
| std::unique_ptr<rive::Renderer> beginFrame( | |
| const FrameOptions& options) override | |
| { | |
| if (options.doClear) | |
| { | |
| testingWindowCanvas2dResetCanvas(options.clearColor); | |
| } | |
| auto renderer = std::make_unique<JSRenderer>(); | |
| renderer->beginFrame(false); | |
| return renderer; | |
| } | |
| void endFrame(std::vector<uint8_t>* pixelData) override | |
| { | |
| // Flush commands so that we can read pixel data from the canvas. | |
| canvasAdvancedModule().call<void>("resolveAnimationFrame"); | |
| // Now that the deferred draw list has run, nothing on the JS side is | |
| // still holding the objects our shims queued via deleteLater(). Embind | |
| // never reclaims these on its own -- class handles created from JS own | |
| // their C++ instance outright, and no finalizer is attached to raw | |
| // pointer handles -- so without this every path, paint and renderer | |
| // would accumulate in canvas_advanced's heap for the whole run. | |
| canvasAdvancedModule().call<void>("flushPendingDeletes"); | |
| if (!pixelData) | |
| { | |
| return; | |
| } | |
| pixelData->resize(m_width * m_height * 4); | |
| if (!testingWindowCanvas2dGetCanvasPixels(pixelData->data(), | |
| m_width, | |
| m_height)) | |
| { | |
| printf("Canvas size mismatch, read failed\n"); | |
| pixelData->assign(pixelData->size(), 0); | |
| return; | |
| } | |
| // getImageData() hands back unpremultiplied RGBA. Multiply by alpha to | |
| // match the other backends. | |
| for (size_t i = 0; i < pixelData->size(); i += 4) | |
| { | |
| uint32_t a = (*pixelData)[i + 3]; | |
| for (size_t c = 0; c < 3; ++c) | |
| { | |
| (*pixelData)[i + c] = | |
| static_cast<uint8_t>(((*pixelData)[i + c] * a + 127) / 255); | |
| } | |
| } | |
| } | |
| private: | |
| std::unique_ptr<JSRenderFactory> m_factory; | |
| }; | |
| }; // namespace rive::gpu | |
| TestingWindow* TestingWindow::MakeCanvas2D() | |
| { | |
| return new rive::gpu::TestingWindowCanvas2D(); | |
| } | |
| #endif |