diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c228c0c..91e2b0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,8 @@ jobs: qt_arch: 'gcc_64' - qt_version: '6.7' qt_arch: 'linux_gcc_64' + - qt_version: '6.8' + qt_arch: 'linux_gcc_64' runs-on: ubuntu-latest diff --git a/.github/workflows/utests.yml b/.github/workflows/utests.yml index bc84c7b..7c12a1a 100644 --- a/.github/workflows/utests.yml +++ b/.github/workflows/utests.yml @@ -18,6 +18,8 @@ jobs: qt_arch: 'gcc_64' - qt_version: '6.7' qt_arch: 'linux_gcc_64' + - qt_version: '6.8' + qt_arch: 'linux_gcc_64' runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d4e5aa..e200f12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.14) -project(scratchcpp-render VERSION 0.6.0 LANGUAGES CXX) +project(scratchcpp-render VERSION 0.10.0 LANGUAGES CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) @@ -11,6 +11,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) option(SCRATCHCPPRENDER_BUILD_UNIT_TESTS "Build unit tests" ON) +if(EMSCRIPTEN) + set(CMAKE_C_FLAGS "-pthread") + set(CMAKE_CXX_FLAGS "-pthread") +endif() + include(build/FindQt.cmake) qt_standard_project_setup(REQUIRES 6.6) diff --git a/README.md b/README.md index 670e6b9..ce57e57 100644 --- a/README.md +++ b/README.md @@ -145,10 +145,10 @@ int main(int argc, char **argv) { - [x] Clones - [x] Sprite dragging - [x] Touching sprite block -- [ ] Touching color blocks (color is touching color is not implemented yet) +- [x] Touching color blocks - [x] Pen blocks - [x] Monitors -- [ ] Graphics effects (color, brightness and ghost are implemented) +- [x] Graphics effects - [x] Speech and thought bubbles - [x] Question text box ("ask and wait" block) diff --git a/libscratchcpp b/libscratchcpp index 363bec1..6c5fc94 160000 --- a/libscratchcpp +++ b/libscratchcpp @@ -1 +1 @@ -Subproject commit 363bec1b8658a25cb558021f09907bdf3b69f80f +Subproject commit 6c5fc94c34ccc9b10cbfdd7a088201643c010518 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fdf033a..aa7735d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,8 @@ qt_add_qml_module(scratchcpp-render projectloader.h projectscene.cpp projectscene.h + targetmodel.cpp + targetmodel.h stagemodel.cpp stagemodel.h spritemodel.cpp @@ -72,10 +74,8 @@ qt_add_qml_module(scratchcpp-render textbubblepainter.h cputexturemanager.cpp cputexturemanager.h - blocks/penextension.cpp - blocks/penextension.h - blocks/penblocks.cpp - blocks/penblocks.h + effecttransform.cpp + effecttransform.h ) list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 2355484..b458ed4 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -11,6 +11,9 @@ ProjectScene { readonly property string fileName: loader.fileName property int stageWidth: 480 property int stageHeight: 360 + readonly property bool running: loader.running + readonly property int renderFps: loader.renderFps + readonly property list unsupportedBlocks: loader.unsupportedBlocks property alias fps: loader.fps property alias turboMode: loader.turboMode property alias cloneLimit: loader.cloneLimit @@ -24,18 +27,24 @@ ProjectScene { readonly property Rectangle stageRect: contentRect signal loaded() signal failedToLoad() + signal loadingAborted() id: root engine: loader.engine stageScale: (stageWidth == 0 || stageHeight == 0) ? 1 : Math.min(width / stageWidth, height / stageHeight) - onFileNameChanged: priv.loading = true; onLoaded: priv.loaded = true onFailedToLoad: priv.loaded = false function load(fileName) { + priv.loading = true; loader.fileName = fileName; } + function stopLoading() { + if (priv.loading) + loader.stopLoading(); + } + QtObject { id: priv property bool loading: false @@ -51,10 +60,27 @@ ProjectScene { onLoadingFinished: { priv.loading = false; - if(loadStatus) - loaded(); - else - failedToLoad(); + switch (loadStatus) { + case ProjectLoader.Loaded: + loaded(); + break; + + case ProjectLoader.Failed: + failedToLoad(); + break; + + case ProjectLoader.Aborted: + loadingAborted(); + break; + + default: + break; + } + } + + onLoadStatusChanged: { + if (loadStatus === ProjectLoader.Loading) + priv.loading = true; } onStageChanged: stage.loadCostume(); @@ -117,6 +143,29 @@ ProjectScene { color: priv.loading || !priv.loaded ? "transparent" : "white" clip: true + Component { + id: renderedTextBubble + + Loader { + property QtObject model: null + active: model ? model.bubbleText !== "" : false + z: model ? model.bubbleLayer : 0 + + sourceComponent: TextBubble { + type: model.bubbleType + text: model.bubbleText + target: model.renderedTarget + stageScale: root.stageScale + stageWidth: root.stageWidth + stageHeight: root.stageHeight + x: target.x + y: target.y + } + + Component.onCompleted: model = modelData + } + } + RenderedTarget { id: stageTarget engine: loader.engine @@ -124,20 +173,13 @@ ProjectScene { mouseArea: sceneMouseArea stageScale: root.stageScale onStageModelChanged: stageModel.renderedTarget = this + Component.onCompleted: stageModel.penLayer = projectPenLayer } Loader { - readonly property alias model: stageTarget.stageModel - active: model ? model.bubbleText !== "" : false - - sourceComponent: TextBubble { - type: model ? model.bubbleType : TextBubbleShape.Say - text: model ? model.bubbleText : "" - target: stageTarget - stageScale: root.stageScale - stageWidth: root.stageWidth - stageHeight: root.stageHeight - } + readonly property alias modelData: stageTarget.stageModel + active: modelData ? modelData.bubbleText !== "" : false + sourceComponent: renderedTextBubble } PenLayer { @@ -150,6 +192,16 @@ ProjectScene { scale: hqPen ? 1 : stageScale transformOrigin: Item.TopLeft visible: !priv.loading + + onWidthChanged: { + if (!hqPen) + refresh(); + } + + onHeightChanged: { + if (!hqPen) + refresh(); + } } Component { @@ -218,33 +270,28 @@ ProjectScene { Component.onCompleted: transform = targetItem.transform[0] } }*/ - - Loader { - readonly property alias model: targetItem.spriteModel - active: model ? model.bubbleText !== "" : false - - sourceComponent: TextBubble { - type: model ? model.bubbleType : TextBubbleShape.Say - text: model ? model.bubbleText : "" - target: targetItem - stageScale: root.stageScale - stageWidth: root.stageWidth - stageHeight: root.stageHeight - } - } } } - Repeater { - id: sprites - model: loader.sprites - delegate: renderedSprite - } + Item { + // Sprites are wrapped in Item to ensure monitors appear above them + Repeater { + id: sprites + model: loader.sprites + delegate: renderedSprite + } - Repeater { - id: clones - model: ListModel {} - delegate: renderedSprite + Repeater { + id: clones + model: ListModel {} + delegate: renderedSprite + } + + Repeater { + id: textBubbles + model: loader.sprites + delegate: renderedTextBubble + } } SceneMouseArea { diff --git a/src/bitmapskin.cpp b/src/bitmapskin.cpp index e499f8a..6acdad0 100644 --- a/src/bitmapskin.cpp +++ b/src/bitmapskin.cpp @@ -36,11 +36,6 @@ BitmapSkin::BitmapSkin(libscratchcpp::Costume *costume) : qWarning() << "invalid bitmap texture (costume name: " + costume->name() + ")"; } -BitmapSkin::~BitmapSkin() -{ - m_texture.release(); -} - Texture BitmapSkin::getTexture(double scale) const { return m_texture; diff --git a/src/bitmapskin.h b/src/bitmapskin.h index 0c9e033..a800975 100644 --- a/src/bitmapskin.h +++ b/src/bitmapskin.h @@ -19,7 +19,6 @@ class BitmapSkin : public Skin { public: BitmapSkin(libscratchcpp::Costume *costume); - ~BitmapSkin(); Texture getTexture(double scale) const override; double getTextureScale(const Texture &texture) const override; diff --git a/src/blocks/penblocks.cpp b/src/blocks/penblocks.cpp deleted file mode 100644 index 5c3e1cd..0000000 --- a/src/blocks/penblocks.cpp +++ /dev/null @@ -1,558 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -#include -#include -#include -#include - -#include "penblocks.h" -#include "penlayer.h" -#include "penstate.h" -#include "spritemodel.h" -#include "stagemodel.h" - -using namespace scratchcpprender; -using namespace libscratchcpp; - -// Pen size range: https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L100-L102 -static const double PEN_SIZE_MIN = 1; -static const double PEN_SIZE_MAX = 1200; - -static const double COLOR_PARAM_MIN = 0; -static const double COLOR_PARAM_MAX = 100; - -const std::unordered_map - PenBlocks::COLOR_PARAM_MAP = { { "color", ColorParam::COLOR }, { "saturation", ColorParam::SATURATION }, { "brightness", ColorParam::BRIGHTNESS }, { "transparency", ColorParam::TRANSPARENCY } }; - -std::string PenBlocks::name() const -{ - return "Pen"; -} - -void PenBlocks::registerBlocks(IEngine *engine) -{ - // Blocks - engine->addCompileFunction(this, "pen_clear", &compileClear); - engine->addCompileFunction(this, "pen_stamp", &compileStamp); - engine->addCompileFunction(this, "pen_penDown", &compilePenDown); - engine->addCompileFunction(this, "pen_penUp", &compilePenUp); - engine->addCompileFunction(this, "pen_setPenColorToColor", &compileSetPenColorToColor); - engine->addCompileFunction(this, "pen_changePenColorParamBy", &compileChangePenColorParamBy); - engine->addCompileFunction(this, "pen_setPenColorParamTo", &compileSetPenColorParamTo); - engine->addCompileFunction(this, "pen_changePenSizeBy", &compileChangePenSizeBy); - engine->addCompileFunction(this, "pen_setPenSizeTo", &compileSetPenSizeTo); - engine->addCompileFunction(this, "pen_changePenShadeBy", &compileChangePenShadeBy); - engine->addCompileFunction(this, "pen_setPenShadeToNumber", &compileSetPenShadeToNumber); - engine->addCompileFunction(this, "pen_changePenHueBy", &compileChangePenHueBy); - engine->addCompileFunction(this, "pen_setPenHueToNumber", &compileSetPenHueToNumber); - - // Inputs - engine->addInput(this, "COLOR", COLOR); - engine->addInput(this, "COLOR_PARAM", COLOR_PARAM); - engine->addInput(this, "VALUE", VALUE); - engine->addInput(this, "SIZE", SIZE); - engine->addInput(this, "SHADE", SHADE); - engine->addInput(this, "HUE", HUE); -} - -void PenBlocks::compileClear(Compiler *compiler) -{ - compiler->addFunctionCall(&clear); -} - -void PenBlocks::compileStamp(Compiler *compiler) -{ - compiler->addFunctionCall(&stamp); -} - -void PenBlocks::compilePenDown(Compiler *compiler) -{ - compiler->addFunctionCall(&penDown); -} - -void PenBlocks::compilePenUp(Compiler *compiler) -{ - compiler->addFunctionCall(&penUp); -} - -void PenBlocks::compileSetPenColorToColor(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(COLOR); - compiler->addFunctionCall(&setPenColorToColor); -} - -void PenBlocks::compileChangePenColorParamBy(libscratchcpp::Compiler *compiler) -{ - Input *input = compiler->input(COLOR_PARAM); - - if (input->pointsToDropdownMenu()) { - std::string value = input->selectedMenuItem(); - BlockFunc f = nullptr; - - if (value == "color") - f = &changePenColorBy; - else if (value == "saturation") - f = &changePenSaturationBy; - else if (value == "brightness") - f = &changePenBrightnessBy; - else if (value == "transparency") - f = &changePenTransparencyBy; - - if (f) { - compiler->addInput(VALUE); - compiler->addFunctionCall(f); - } - } else { - compiler->addInput(input); - compiler->addInput(VALUE); - compiler->addFunctionCall(&changePenColorParamBy); - } -} - -void PenBlocks::compileSetPenColorParamTo(Compiler *compiler) -{ - Input *input = compiler->input(COLOR_PARAM); - - if (input->pointsToDropdownMenu()) { - std::string value = input->selectedMenuItem(); - BlockFunc f = nullptr; - - if (value == "color") - f = &setPenColorTo; - else if (value == "saturation") - f = &setPenSaturationTo; - else if (value == "brightness") - f = &setPenBrightnessTo; - else if (value == "transparency") - f = &setPenTransparencyTo; - - if (f) { - compiler->addInput(VALUE); - compiler->addFunctionCall(f); - } - } else { - compiler->addInput(input); - compiler->addInput(VALUE); - compiler->addFunctionCall(&setPenColorParamTo); - } -} - -void PenBlocks::compileChangePenSizeBy(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(SIZE); - compiler->addFunctionCall(&changePenSizeBy); -} - -void PenBlocks::compileSetPenSizeTo(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(SIZE); - compiler->addFunctionCall(&setPenSizeTo); -} - -void PenBlocks::compileChangePenShadeBy(Compiler *compiler) -{ - compiler->addInput(SHADE); - compiler->addFunctionCall(&changePenShadeBy); -} - -void PenBlocks::compileSetPenShadeToNumber(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(SHADE); - compiler->addFunctionCall(&setPenShadeToNumber); -} - -void PenBlocks::compileChangePenHueBy(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(HUE); - compiler->addFunctionCall(&changePenHueBy); -} - -void PenBlocks::compileSetPenHueToNumber(libscratchcpp::Compiler *compiler) -{ - compiler->addInput(HUE); - compiler->addFunctionCall(&setPenHueToNumber); -} - -unsigned int PenBlocks::clear(VirtualMachine *vm) -{ - IPenLayer *penLayer = PenLayer::getProjectPenLayer(vm->engine()); - - if (penLayer) { - penLayer->clear(); - vm->engine()->requestRedraw(); - } - - return 0; -} - -unsigned int PenBlocks::stamp(libscratchcpp::VirtualMachine *vm) -{ - IPenLayer *penLayer = PenLayer::getProjectPenLayer(vm->engine()); - - if (penLayer) { - Target *target = vm->target(); - IRenderedTarget *renderedTarget = nullptr; - - if (target->isStage()) { - IStageHandler *iface = static_cast(target)->getInterface(); - renderedTarget = static_cast(iface)->renderedTarget(); - } else { - ISpriteHandler *iface = static_cast(target)->getInterface(); - renderedTarget = static_cast(iface)->renderedTarget(); - } - - penLayer->stamp(renderedTarget); - vm->engine()->requestRedraw(); - } - - return 0; -} - -unsigned int PenBlocks::penDown(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - model->setPenDown(true); - - return 0; -} - -unsigned int PenBlocks::penUp(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - model->setPenDown(false); - - return 0; -} - -unsigned int PenBlocks::changePenSizeBy(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - model->penAttributes().diameter = std::clamp(model->penAttributes().diameter + vm->getInput(0, 1)->toDouble(), PEN_SIZE_MIN, PEN_SIZE_MAX); - - return 1; -} - -unsigned int PenBlocks::setPenSizeTo(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - model->penAttributes().diameter = std::clamp(vm->getInput(0, 1)->toDouble(), PEN_SIZE_MIN, PEN_SIZE_MAX); - - return 1; -} - -unsigned int PenBlocks::changePenShadeBy(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - PenState &penState = model->penState(); - setPenShade(penState.shade + vm->getInput(0, 1)->toDouble(), penState); - } - - return 1; -} - -unsigned int PenBlocks::setPenShadeToNumber(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setPenShade(vm->getInput(0, 1)->toInt(), model->penState()); - - return 1; -} - -unsigned int PenBlocks::changePenHueBy(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - PenState &penState = model->penState(); - const double colorChange = vm->getInput(0, 1)->toDouble() / 2; - setOrChangeColorParam(ColorParam::COLOR, colorChange, penState, true); - legacyUpdatePenColor(penState); - } - - return 1; -} - -unsigned int PenBlocks::setPenHueToNumber(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - PenState &penState = model->penState(); - const double colorValue = vm->getInput(0, 1)->toDouble() / 2; - setOrChangeColorParam(ColorParam::COLOR, colorValue, penState, false); - penState.transparency = 0; - legacyUpdatePenColor(penState); - } - - return 1; -} - -unsigned int PenBlocks::setPenColorToColor(libscratchcpp::VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - const Value *value = vm->getInput(0, 1); - std::string stringValue; - PenState &penState = model->penState(); - QColor newColor; - - if (value->isString()) - stringValue = value->toString(); - - if (!stringValue.empty() && stringValue[0] == '#') { - bool valid = false; - - if (stringValue.size() <= 7) // #RRGGBB - { - newColor = QColor::fromString(stringValue); - valid = newColor.isValid(); - } - - if (!valid) - newColor = Qt::black; - - } else - newColor = QColor::fromRgba(static_cast(value->toLong())); - - QColor hsv = newColor.toHsv(); - penState.color = (hsv.hue() / 360.0) * 100; - penState.saturation = hsv.saturationF() * 100; - penState.brightness = hsv.valueF() * 100; - - if (newColor.alpha() > 0) - penState.transparency = 100 * (1 - newColor.alpha() / 255.0); - else - penState.transparency = 0; - - penState.updateColor(); - - // Set the legacy "shade" value the same way Scratch 2 did. - penState.shade = penState.brightness / 2; - } - - return 1; -} - -unsigned int PenBlocks::changePenColorParamBy(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - const auto it = COLOR_PARAM_MAP.find(vm->getInput(0, 2)->toString()); - - if (it == COLOR_PARAM_MAP.cend()) - return 2; - - setOrChangeColorParam(it->second, vm->getInput(1, 2)->toDouble(), model->penState(), true); - } - - return 2; -} - -unsigned int PenBlocks::changePenColorBy(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::COLOR, vm->getInput(0, 1)->toDouble(), model->penState(), true); - - return 1; -} - -unsigned int PenBlocks::changePenSaturationBy(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::SATURATION, vm->getInput(0, 1)->toDouble(), model->penState(), true); - - return 1; -} - -unsigned int PenBlocks::changePenBrightnessBy(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::BRIGHTNESS, vm->getInput(0, 1)->toDouble(), model->penState(), true); - - return 1; -} - -unsigned int PenBlocks::changePenTransparencyBy(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::TRANSPARENCY, vm->getInput(0, 1)->toDouble(), model->penState(), true); - - return 1; -} - -unsigned int PenBlocks::setPenColorParamTo(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) { - const auto it = COLOR_PARAM_MAP.find(vm->getInput(0, 2)->toString()); - - if (it == COLOR_PARAM_MAP.cend()) - return 2; - - setOrChangeColorParam(it->second, vm->getInput(1, 2)->toDouble(), model->penState(), false); - } - - return 2; -} - -unsigned int PenBlocks::setPenColorTo(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::COLOR, vm->getInput(0, 1)->toDouble(), model->penState(), false); - - return 1; -} - -unsigned int PenBlocks::setPenSaturationTo(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::SATURATION, vm->getInput(0, 1)->toDouble(), model->penState(), false); - - return 1; -} - -unsigned int PenBlocks::setPenBrightnessTo(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::BRIGHTNESS, vm->getInput(0, 1)->toDouble(), model->penState(), false); - - return 1; -} - -unsigned int PenBlocks::setPenTransparencyTo(VirtualMachine *vm) -{ - SpriteModel *model = getSpriteModel(vm); - - if (model) - setOrChangeColorParam(ColorParam::TRANSPARENCY, vm->getInput(0, 1)->toDouble(), model->penState(), false); - - return 1; -} - -SpriteModel *PenBlocks::getSpriteModel(libscratchcpp::VirtualMachine *vm) -{ - Target *target = vm->target(); - - if (!target || target->isStage()) - return nullptr; - - Sprite *sprite = static_cast(target); - SpriteModel *model = static_cast(sprite->getInterface()); - return model; -} - -void PenBlocks::setOrChangeColorParam(ColorParam param, double value, PenState &penState, bool change) -{ - switch (param) { - case ColorParam::COLOR: - penState.color = wrapClamp(value + (change ? penState.color : 0), 0, 100); - break; - - case ColorParam::SATURATION: - penState.saturation = std::clamp(value + (change ? penState.saturation : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); - break; - - case ColorParam::BRIGHTNESS: - penState.brightness = std::clamp(value + (change ? penState.brightness : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); - break; - - case ColorParam::TRANSPARENCY: - penState.transparency = std::clamp(value + (change ? penState.transparency : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); - break; - - default: - assert(false); - return; - } - - penState.updateColor(); -} - -void PenBlocks::setPenShade(int shade, PenState &penState) -{ - // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L718-L730 - // Wrap clamp the new shade value the way Scratch 2 did - shade = shade % 200; - - if (shade < 0) - shade += 200; - - // And store the shade that was used to compute this new color for later use - penState.shade = shade; - - legacyUpdatePenColor(penState); -} - -void PenBlocks::legacyUpdatePenColor(PenState &penState) -{ - // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L750-L767 - // Create the new color in RGB using the scratch 2 "shade" model - QRgb rgb = QColor::fromHsvF(penState.color / 100, 1, 1).rgb(); - const double shade = (penState.shade > 100) ? 200 - penState.shade : penState.shade; - - if (shade < 50) - rgb = mixRgb(0, rgb, (10 + shade) / 60); - else - rgb = mixRgb(rgb, 0xFFFFFF, (shade - 50) / 60); - - // Update the pen state according to new color - QColor hsv = QColor::fromRgb(rgb).toHsv(); - penState.color = 100 * hsv.hueF(); - penState.saturation = 100 * hsv.saturationF(); - penState.brightness = 100 * hsv.valueF(); - - penState.updateColor(); -} - -double PenBlocks::wrapClamp(double n, double min, double max) -{ - // TODO: Move this to a separate class - const double range = max - min /*+ 1*/; - return n - (std::floor((n - min) / range) * range); -} - -QRgb PenBlocks::mixRgb(QRgb rgb0, QRgb rgb1, double fraction1) -{ - // https://github.com/scratchfoundation/scratch-vm/blob/a4f095db5e03e072ba222fe721eeeb543c9b9c15/src/util/color.js#L192-L201 - // https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/util/Color.as#L75-L89 - if (fraction1 <= 0) - return rgb0; - - if (fraction1 >= 1) - return rgb1; - - const double fraction0 = 1 - fraction1; - const int r = static_cast(((fraction0 * qRed(rgb0)) + (fraction1 * qRed(rgb1)))) & 255; - const int g = static_cast(((fraction0 * qGreen(rgb0)) + (fraction1 * qGreen(rgb1)))) & 255; - const int b = static_cast(((fraction0 * qBlue(rgb0)) + (fraction1 * qBlue(rgb1)))) & 255; - return qRgb(r, g, b); -} diff --git a/src/blocks/penblocks.h b/src/blocks/penblocks.h deleted file mode 100644 index 53f6596..0000000 --- a/src/blocks/penblocks.h +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -#pragma once - -#include -#include - -namespace scratchcpprender -{ - -class SpriteModel; -class PenState; - -class PenBlocks : public libscratchcpp::IBlockSection -{ - public: - enum Inputs - { - COLOR, - COLOR_PARAM, - VALUE, - SIZE, - SHADE, - HUE - }; - - std::string name() const override; - - void registerBlocks(libscratchcpp::IEngine *engine) override; - - static void compileClear(libscratchcpp::Compiler *compiler); - static void compileStamp(libscratchcpp::Compiler *compiler); - static void compilePenDown(libscratchcpp::Compiler *compiler); - static void compilePenUp(libscratchcpp::Compiler *compiler); - static void compileSetPenColorToColor(libscratchcpp::Compiler *compiler); - static void compileChangePenColorParamBy(libscratchcpp::Compiler *compiler); - static void compileSetPenColorParamTo(libscratchcpp::Compiler *compiler); - static void compileChangePenSizeBy(libscratchcpp::Compiler *compiler); - static void compileSetPenSizeTo(libscratchcpp::Compiler *compiler); - static void compileChangePenShadeBy(libscratchcpp::Compiler *compiler); - static void compileSetPenShadeToNumber(libscratchcpp::Compiler *compiler); - static void compileChangePenHueBy(libscratchcpp::Compiler *compiler); - static void compileSetPenHueToNumber(libscratchcpp::Compiler *compiler); - - static unsigned int clear(libscratchcpp::VirtualMachine *vm); - static unsigned int stamp(libscratchcpp::VirtualMachine *vm); - static unsigned int penDown(libscratchcpp::VirtualMachine *vm); - static unsigned int penUp(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenColorToColor(libscratchcpp::VirtualMachine *vm); - - static unsigned int changePenColorParamBy(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenColorBy(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenSaturationBy(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenBrightnessBy(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenTransparencyBy(libscratchcpp::VirtualMachine *vm); - - static unsigned int setPenColorParamTo(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenColorTo(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenSaturationTo(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenBrightnessTo(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenTransparencyTo(libscratchcpp::VirtualMachine *vm); - - static unsigned int changePenSizeBy(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenSizeTo(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenShadeBy(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenShadeToNumber(libscratchcpp::VirtualMachine *vm); - static unsigned int changePenHueBy(libscratchcpp::VirtualMachine *vm); - static unsigned int setPenHueToNumber(libscratchcpp::VirtualMachine *vm); - - private: - enum class ColorParam - { - COLOR, - SATURATION, - BRIGHTNESS, - TRANSPARENCY - }; - - static SpriteModel *getSpriteModel(libscratchcpp::VirtualMachine *vm); - static void setOrChangeColorParam(ColorParam param, double value, PenState &penState, bool change); - static void setPenShade(int shade, PenState &penState); - static void legacyUpdatePenColor(PenState &penState); - static double wrapClamp(double n, double min, double max); - static QRgb mixRgb(QRgb rgb0, QRgb rgb1, double fraction1); - - static const std::unordered_map COLOR_PARAM_MAP; -}; - -} // namespace scratchcpprender diff --git a/src/blocks/penextension.cpp b/src/blocks/penextension.cpp deleted file mode 100644 index 1226d08..0000000 --- a/src/blocks/penextension.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -#include - -#include "penextension.h" -#include "penblocks.h" - -using namespace scratchcpprender; -using namespace libscratchcpp; - -std::string PenExtension::name() const -{ - return "pen"; -} - -std::string PenExtension::description() const -{ - return "Pen extension"; -} - -void PenExtension::registerSections(IEngine *engine) -{ - engine->registerSection(std::make_shared()); -} diff --git a/src/blocks/penextension.h b/src/blocks/penextension.h deleted file mode 100644 index 1e7363d..0000000 --- a/src/blocks/penextension.h +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -#pragma once - -#include - -namespace scratchcpprender -{ - -class PenExtension : public libscratchcpp::IExtension -{ - public: - std::string name() const override; - std::string description() const override; - - void registerSections(libscratchcpp::IEngine *engine) override; -}; - -} // namespace scratchcpprender diff --git a/src/cputexturemanager.cpp b/src/cputexturemanager.cpp index c97b37d..3193358 100644 --- a/src/cputexturemanager.cpp +++ b/src/cputexturemanager.cpp @@ -2,6 +2,7 @@ #include "cputexturemanager.h" #include "texture.h" +#include "effecttransform.h" using namespace scratchcpprender; @@ -32,29 +33,96 @@ GLubyte *CpuTextureManager::getTextureData(const Texture &texture) return it->second; } -const std::vector &CpuTextureManager::getTextureConvexHullPoints(const Texture &texture) +void CpuTextureManager::getTextureConvexHullPoints( + const Texture &texture, + const QSize &skinSize, + ShaderManager::Effect effectMask, + const std::unordered_map &effects, + std::vector &dst) { - static const std::vector empty; + dst.clear(); if (!texture.isValid()) - return empty; + return; - const GLuint handle = texture.handle(); - auto it = m_convexHullPoints.find(handle); + // Remove effects that don't change shape + if (effectMask != 0) { + const auto &allEffects = ShaderManager::effects(); - if (it == m_convexHullPoints.cend()) { - if (addTexture(texture)) - return m_convexHullPoints[handle]; - else - return empty; + for (ShaderManager::Effect effect : allEffects) { + if ((effectMask & effect) != 0 && !ShaderManager::effectShapeChanges(effect)) + effectMask &= ~effect; + } + } + + // If there are no shape-changing effects, use cached hull points + if (effectMask == 0) { + const GLuint handle = texture.handle(); + auto it = m_convexHullPoints.find(handle); + + if (it == m_convexHullPoints.cend()) { + if (addTexture(texture)) + dst = m_convexHullPoints[handle]; + } else + dst = it->second; } else - return it->second; + readTexture(texture, skinSize, effectMask, effects, nullptr, dst); +} + +QRgb CpuTextureManager::getPointColor(const Texture &texture, int x, int y, ShaderManager::Effect effectMask, const std::unordered_map &effects) +{ + const int width = texture.width(); + const int height = texture.height(); + + if (effectMask != 0) { + // Get local position with effect transform + QVector2D transformedCoords; + const QVector2D localCoords(x / static_cast(width), y / static_cast(height)); + EffectTransform::transformPoint(effectMask, effects, texture.size(), localCoords, transformedCoords); + x = transformedCoords.x() * width; + y = transformedCoords.y() * height; + } + + if ((x < 0 || x >= width) || (y < 0 || y >= height)) + return qRgba(0, 0, 0, 0); + + GLubyte *pixels = getTextureData(texture); + QRgb color = qRgba(pixels[(y * width + x) * 4], pixels[(y * width + x) * 4 + 1], pixels[(y * width + x) * 4 + 2], pixels[(y * width + x) * 4 + 3]); + + if (effectMask == 0) + return color; + else + return EffectTransform::transformColor(effectMask, effects, color); } -bool CpuTextureManager::textureContainsPoint(const Texture &texture, const QPointF &localPoint) +bool CpuTextureManager::textureContainsPoint(const Texture &texture, const QPointF &localPoint, ShaderManager::Effect effectMask, const std::unordered_map &effects) { // https://github.com/scratchfoundation/scratch-render/blob/7b823985bc6fe92f572cc3276a8915e550f7c5e6/src/Silhouette.js#L219-L226 - return getPointAlpha(texture, localPoint.x(), localPoint.y()) > 0; + const int width = texture.width(); + const int height = texture.height(); + int x = localPoint.x(); + int y = localPoint.y(); + + if (effectMask != 0) { + // Get local position with effect transform + QVector2D transformedCoords; + const QVector2D localCoords(x / static_cast(width), y / static_cast(height)); + EffectTransform::transformPoint(effectMask, effects, texture.size(), localCoords, transformedCoords); + x = transformedCoords.x() * width; + y = transformedCoords.y() * height; + } + + if ((x < 0 || x >= width) || (y < 0 || y >= height)) + return false; + + GLubyte *pixels = getTextureData(texture); + + if (pixels) { + QRgb color = qRgba(pixels[(y * width + x) * 4], pixels[(y * width + x) * 4 + 1], pixels[(y * width + x) * 4 + 2], pixels[(y * width + x) * 4 + 3]); + return qAlpha(color) > 0; + } + + return false; } void CpuTextureManager::removeTexture(const Texture &texture) @@ -72,7 +140,24 @@ void CpuTextureManager::removeTexture(const Texture &texture) } } -bool CpuTextureManager::addTexture(const Texture &texture) +bool CpuTextureManager::addTexture(const Texture &tex) +{ + if (!tex.isValid()) + return false; + + const GLuint handle = tex.handle(); + m_textureData[handle] = nullptr; + m_convexHullPoints[handle] = {}; + return readTexture(tex, QSize(), ShaderManager::Effect::NoEffect, {}, &m_textureData[handle], m_convexHullPoints[handle]); +} + +bool CpuTextureManager::readTexture( + const Texture &texture, + const QSize &skinSize, + ShaderManager::Effect effectMask, + const std::unordered_map &effects, + GLubyte **data, + std::vector &points) const { if (!texture.isValid()) return false; @@ -84,15 +169,27 @@ bool CpuTextureManager::addTexture(const Texture &texture) QOpenGLFunctions glF; glF.initializeOpenGLFunctions(); - // Create a FBO for the texture - unsigned int fbo; - glF.glGenFramebuffers(1, &fbo); - glF.glBindFramebuffer(GL_FRAMEBUFFER, fbo); + // Create global FBO + if (m_fbo == 0) { + glF.glGenFramebuffers(1, &m_fbo); + + QObject::connect(QOpenGLContext::currentContext(), &QOpenGLContext::aboutToBeDestroyed, []() { + if (QOpenGLContext::currentContext()) { + QOpenGLFunctions glF; + glF.initializeOpenGLFunctions(); + glF.glDeleteFramebuffers(1, &m_fbo); + m_fbo = 0; + } + }); + } + + // Bind the texture to the global FBO + glF.glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, handle, 0); if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { qWarning() << "error: framebuffer incomplete (CpuTextureManager)"; - glF.glDeleteFramebuffers(1, &fbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); return false; } @@ -100,49 +197,136 @@ bool CpuTextureManager::addTexture(const Texture &texture) GLubyte *pixels = new GLubyte[width * height * 4]; // 4 channels (RGBA) glF.glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - // Flip vertically - int rowSize = width * 4; - GLubyte *tempRow = new GLubyte[rowSize]; + std::vector leftHull; + std::vector rightHull; + leftHull.reserve(height); + rightHull.reserve(height); - for (size_t i = 0; i < height / 2; ++i) { - size_t topRowIndex = i * rowSize; - size_t bottomRowIndex = (height - 1 - i) * rowSize; - - // Swap rows - memcpy(tempRow, &pixels[topRowIndex], rowSize); - memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize); - memcpy(&pixels[bottomRowIndex], tempRow, rowSize); + for (int x = 0; x < height; x++) { + leftHull.push_back(QPoint(-1, -1)); + rightHull.push_back(QPoint(-1, -1)); } - delete[] tempRow; + int leftEndPointIndex = -1; + int rightEndPointIndex = -1; - m_textureData[handle] = pixels; - m_convexHullPoints[handle] = {}; - std::vector &hullPoints = m_convexHullPoints[handle]; + auto determinant = [](const QPoint &A, const QPoint &B, const QPoint &C) { return (B.x() - A.x()) * (C.y() - A.y()) - (B.y() - A.y()) * (C.x() - A.x()); }; - // Get convex hull points + // Get convex hull points (flipped vertically) + // https://github.com/scratchfoundation/scratch-render/blob/0f6663f3148b4f994d58e19590e14c152f1cc2f8/src/RenderWebGL.js#L1829-L1955 for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = (y * width + x) * 4; // 4 channels (RGBA) + QPoint currentPoint; + int x; + const int flippedY = height - 1 - y; + + for (x = 0; x < width; x++) { + int transformedX = x; + int transformedY = flippedY; + + if (effectMask != 0) { + // Get local position with effect transform + QVector2D transformedCoords; + const QVector2D localCoords(transformedX / static_cast(width), transformedY / static_cast(height)); + EffectTransform::transformPoint(effectMask, effects, skinSize, localCoords, transformedCoords); + transformedX = transformedCoords.x() * width; + transformedY = transformedCoords.y() * height; + } + + if ((transformedX >= 0 && transformedX < width) && (transformedY >= 0 && transformedY < height)) { + int index = (transformedY * width + transformedX) * 4; + + if (pixels[index + 3] > 0) { + currentPoint.setX(x); + currentPoint.setY(y); + break; + } + } + } + + if (x >= width) + continue; + + while (leftEndPointIndex > 0) { + if (determinant(leftHull[leftEndPointIndex], leftHull[leftEndPointIndex - 1], currentPoint) > 0) + break; + else { + leftEndPointIndex--; + } + } - // Check alpha channel - if (pixels[index + 3] > 0) - hullPoints.push_back(QPoint(x, y)); + leftHull[++leftEndPointIndex] = currentPoint; + + for (x = width - 1; x >= 0; x--) { + int transformedX = x; + int transformedY = flippedY; + + if (effectMask != 0) { + // Get local position with effect transform + QVector2D transformedCoords; + const QVector2D localCoords(transformedX / static_cast(width), transformedY / static_cast(height)); + EffectTransform::transformPoint(effectMask, effects, skinSize, localCoords, transformedCoords); + transformedX = transformedCoords.x() * width; + transformedY = transformedCoords.y() * height; + } + + if ((transformedX >= 0 && transformedX < width) && (transformedY >= 0 && transformedY < height)) { + int index = (transformedY * width + transformedX) * 4; + + if (pixels[index + 3] > 0) { + currentPoint.setX(x); + currentPoint.setY(y); + break; + } + } } + + while (rightEndPointIndex > 0) { + if (determinant(rightHull[rightEndPointIndex], rightHull[rightEndPointIndex - 1], currentPoint) < 0) + break; + else + rightEndPointIndex--; + } + + rightHull[++rightEndPointIndex] = currentPoint; + } + + points.clear(); + points.reserve((leftEndPointIndex + 1) + (rightEndPointIndex + 1)); + + long i; + + for (i = 0; i < leftHull.size(); i++) { + if (leftHull[i].x() >= 0) + points.push_back(leftHull[i]); } + for (i = rightEndPointIndex; i >= 0; --i) + if (rightHull[i].x() >= 0) + points.push_back(rightHull[i]); + + if (data) { + // Flip vertically + int rowSize = width * 4; + GLubyte *tempRow = new GLubyte[rowSize]; + + for (size_t i = 0; i < height / 2; ++i) { + size_t topRowIndex = i * rowSize; + size_t bottomRowIndex = (height - 1 - i) * rowSize; + + // Swap rows + memcpy(tempRow, &pixels[topRowIndex], rowSize); + memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize); + memcpy(&pixels[bottomRowIndex], tempRow, rowSize); + } + + delete[] tempRow; + + *data = pixels; + } else + delete[] pixels; + // Cleanup glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); - glF.glDeleteFramebuffers(1, &fbo); return true; } - -int CpuTextureManager::getPointAlpha(const Texture &texture, int x, int y) -{ - if ((x < 0 || x >= texture.width()) || (y < 0 || y >= texture.height())) - return 0; - - GLubyte *pixels = getTextureData(texture); - return pixels[(y * texture.width() + x) * 4 + 3]; -} diff --git a/src/cputexturemanager.h b/src/cputexturemanager.h index df19588..ebe047f 100644 --- a/src/cputexturemanager.h +++ b/src/cputexturemanager.h @@ -6,6 +6,8 @@ #include #include +#include "shadermanager.h" + namespace scratchcpprender { @@ -18,16 +20,29 @@ class CpuTextureManager ~CpuTextureManager(); GLubyte *getTextureData(const Texture &texture); - const std::vector &getTextureConvexHullPoints(const Texture &texture); + void getTextureConvexHullPoints( + const Texture &texture, + const QSize &skinSize, + ShaderManager::Effect effectMask, + const std::unordered_map &effects, + std::vector &dst); - bool textureContainsPoint(const Texture &texture, const QPointF &localPoint); + QRgb getPointColor(const Texture &texture, int x, int y, ShaderManager::Effect effectMask, const std::unordered_map &effects); + bool textureContainsPoint(const Texture &texture, const QPointF &localPoint, ShaderManager::Effect effectMask, const std::unordered_map &effects); void removeTexture(const Texture &texture); private: - bool addTexture(const Texture &texture); - int getPointAlpha(const Texture &texture, int x, int y); - + bool addTexture(const Texture &tex); + bool readTexture( + const Texture &texture, + const QSize &skinSize, + ShaderManager::Effect effectMask, + const std::unordered_map &effects, + GLubyte **data, + std::vector &points) const; + + static inline GLuint m_fbo = 0; // single FBO for all texture managers std::unordered_map m_textureData; std::unordered_map> m_convexHullPoints; }; diff --git a/src/effecttransform.cpp b/src/effecttransform.cpp new file mode 100644 index 0000000..8be1e8c --- /dev/null +++ b/src/effecttransform.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "effecttransform.h" + +using namespace scratchcpprender; + +// A texture coordinate is between 0 and 1, 0.5 is the center position +static const float CENTER_X = 0.5f; +static const float CENTER_Y = 0.5f; + +inline float fract(float x) +{ + // https://registry.khronos.org/OpenGL-Refpages/gl4/html/fract.xhtml + return x - std::floor(x); +} + +QRgb EffectTransform::transformColor(ShaderManager::Effect effectMask, const std::unordered_map &effectValues, QRgb color) +{ + // https://github.com/scratchfoundation/scratch-render/blob/e075e5f5ebc95dec4a2718551624ad587c56f0a6/src/EffectTransform.js#L40-L119 + // If the color is fully transparent, don't bother attempting any transformations. + if (qAlpha(color) == 0) + return color; + + QColor inOutColor = QColor::fromRgba(color); + + std::unordered_map uniforms; + ShaderManager::getUniformValuesForEffects(effectValues, uniforms); + + const bool enableColor = (effectMask & ShaderManager::Effect::Color) != 0; + const bool enableBrightness = (effectMask & ShaderManager::Effect::Brightness) != 0; + + if (enableColor || enableBrightness) { + // gl_FragColor.rgb /= gl_FragColor.a + epsilon; + // Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated + // for partially transparent pixels. + const float alpha = inOutColor.alphaF(); + + if (alpha == 0) { + inOutColor.setRed(255); + inOutColor.setGreen(255); + inOutColor.setBlue(255); + } else { + inOutColor.setRedF(inOutColor.redF() / alpha); + inOutColor.setGreenF(inOutColor.greenF() / alpha); + inOutColor.setBlueF(inOutColor.blueF() / alpha); + } + + if (enableColor) { + // vec3 hsv = convertRGB2HSV(gl_FragColor.xyz); + QColor hsv = inOutColor.toHsv(); + + // this code forces grayscale values to be slightly saturated + // so that some slight change of hue will be visible + // const float minLightness = 0.11 / 2.0; + const float minV = 0.11f / 2.0f; + // const float minSaturation = 0.09; + const float minS = 0.09f; + // if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness); + if (hsv.valueF() < minV) { + hsv.setHsvF(0.0f, 1.0f, minV); + // else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z); + } else if (hsv.saturationF() < minS) { + hsv.setHsvF(0.0f, minS, hsv.valueF()); + } + + // hsv.x = mod(hsv.x + u_color, 1.0); + // if (hsv.x < 0.0) hsv.x += 1.0; + float hue = std::fmod(uniforms[ShaderManager::Effect::Color] + hsv.hueF(), 1.0f); + + if (hue < 0.0f) + hue += 1.0f; + + hsv.setHsvF(hue, hsv.saturationF(), hsv.valueF()); + + // gl_FragColor.rgb = convertHSV2RGB(hsl); + inOutColor = hsv.toRgb(); + } + + if (enableBrightness) { + const float brightness = uniforms[ShaderManager::Effect::Brightness] * 255.0f; + // gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1)); + inOutColor.setRed(std::clamp(inOutColor.red() + brightness, 0.0f, 255.0f)); + inOutColor.setGreen(std::clamp(inOutColor.green() + brightness, 0.0f, 255.0f)); + inOutColor.setBlue(std::clamp(inOutColor.blue() + brightness, 0.0f, 255.0f)); + } + + // gl_FragColor.rgb *= gl_FragColor.a + epsilon; + // Now we're doing the reverse, premultiplying by the alpha once again. + inOutColor.setRedF(inOutColor.redF() * alpha); + inOutColor.setGreenF(inOutColor.greenF() * alpha); + inOutColor.setBlueF(inOutColor.blueF() * alpha); + + // Restore alpha + inOutColor.setAlphaF(alpha); + } + + const float ghost = uniforms[ShaderManager::Effect::Ghost]; + + if (ghost != 1) { + // gl_FragColor *= u_ghost + inOutColor.setRedF(inOutColor.redF() * ghost); + inOutColor.setGreenF(inOutColor.greenF() * ghost); + inOutColor.setBlueF(inOutColor.blueF() * ghost); + inOutColor.setAlphaF(inOutColor.alphaF() * ghost); + } + + return inOutColor.rgba(); +} + +void EffectTransform::transformPoint(ShaderManager::Effect effectMask, const std::unordered_map &effectValues, const QSize &size, const QVector2D &vec, QVector2D &dst) +{ + // https://github.com/scratchfoundation/scratch-render/blob/e075e5f5ebc95dec4a2718551624ad587c56f0a6/src/EffectTransform.js#L128-L194 + dst = vec; + + std::unordered_map uniforms; + ShaderManager::getUniformValuesForEffects(effectValues, uniforms); + + if ((effectMask & ShaderManager::Effect::Mosaic) != 0) { + // texcoord0 = fract(u_mosaic * texcoord0); + const float mosaic = uniforms[ShaderManager::Effect::Mosaic]; + dst.setX(fract(mosaic * dst.x())); + dst.setY(fract(mosaic * dst.y())); + } + + if ((effectMask & ShaderManager::Effect::Pixelate) != 0) { + // vec2 pixelTexelSize = u_skinSize / u_pixelate; + const float pixelate = uniforms[ShaderManager::Effect::Pixelate]; + const float texelX = size.width() / pixelate; + const float texelY = size.height() / pixelate; + // texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / + // pixelTexelSize; + dst.setX((std::floor(dst.x() * texelX) + CENTER_X) / texelX); + dst.setY((std::floor(dst.y() * texelY) + CENTER_Y) / texelY); + } + + if ((effectMask & ShaderManager::Effect::Whirl) != 0) { + const float whirl = uniforms[ShaderManager::Effect::Whirl]; + // const float kRadius = 0.5; + const float RADIUS = 0.5f; + // vec2 offset = texcoord0 - kCenter; + const float offsetX = dst.x() - CENTER_X; + const float offsetY = dst.y() - CENTER_Y; + // float offsetMagnitude = length(offset); + const float offsetMagnitude = std::sqrt(std::pow(offsetX, 2.0f) + std::pow(offsetY, 2.0f)); + // float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0); + const float whirlFactor = std::max(1.0f - (offsetMagnitude / RADIUS), 0.0f); + // float whirlActual = u_whirl * whirlFactor * whirlFactor; + const float whirlActual = whirl * whirlFactor * whirlFactor; + // float sinWhirl = sin(whirlActual); + const float sinWhirl = std::sin(whirlActual); + // float cosWhirl = cos(whirlActual); + const float cosWhirl = std::cos(whirlActual); + // mat2 rotationMatrix = mat2( + // cosWhirl, -sinWhirl, + // sinWhirl, cosWhirl + // ); + const float rot1 = cosWhirl; + const float rot2 = -sinWhirl; + const float rot3 = sinWhirl; + const float rot4 = cosWhirl; + + // texcoord0 = rotationMatrix * offset + kCenter; + dst.setX((rot1 * offsetX) + (rot3 * offsetY) + CENTER_X); + dst.setY((rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y); + } + + if ((effectMask & ShaderManager::Effect::Fisheye) != 0) { + const float fisheye = uniforms[ShaderManager::Effect::Fisheye]; + // vec2 vec = (texcoord0 - kCenter) / kCenter; + const float vX = (dst.x() - CENTER_X) / CENTER_X; + const float vY = (dst.y() - CENTER_Y) / CENTER_Y; + // float vecLength = length(vec); + const float vLength = std::sqrt((vX * vX) + (vY * vY)); + // float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength); + const float r = std::pow(std::min(vLength, 1.0f), fisheye) * std::max(1.0f, vLength); + // vec2 unit = vec / vecLength; + const float unitX = vX / vLength; + const float unitY = vY / vLength; + // texcoord0 = kCenter + r * unit * kCenter; + dst.setX(CENTER_X + (r * unitX * CENTER_X)); + dst.setY(CENTER_Y + (r * unitY * CENTER_Y)); + } +} diff --git a/src/effecttransform.h b/src/effecttransform.h new file mode 100644 index 0000000..0a4e74a --- /dev/null +++ b/src/effecttransform.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "shadermanager.h" + +namespace scratchcpprender +{ + +class EffectTransform +{ + public: + EffectTransform() = delete; + + static QRgb transformColor(ShaderManager::Effect effectMask, const std::unordered_map &effectValues, QRgb color); + static void transformPoint(ShaderManager::Effect effectMask, const std::unordered_map &effectValues, const QSize &size, const QVector2D &vec, QVector2D &dst); +}; + +} // namespace scratchcpprender diff --git a/src/global_functions.cpp b/src/global_functions.cpp index 3dc93a1..9338197 100644 --- a/src/global_functions.cpp +++ b/src/global_functions.cpp @@ -7,6 +7,14 @@ void scratchcpprender::init() { qputenv("QSG_RENDER_LOOP", "basic"); QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setSwapInterval(0); +#ifdef Q_OS_MACOS + format.setProfile(QSurfaceFormat::CoreProfile); + format.setVersion(3, 2); +#endif + QSurfaceFormat::setDefaultFormat(format); } const std::string &scratchcpprender::version() diff --git a/src/graphicseffect.cpp b/src/graphicseffect.cpp index cb07d26..e78384c 100644 --- a/src/graphicseffect.cpp +++ b/src/graphicseffect.cpp @@ -4,6 +4,11 @@ using namespace scratchcpprender; +static std::unordered_map> EFFECT_RANGE = { + { ShaderManager::Effect::Ghost, { 0, 100 } }, + { ShaderManager::Effect::Brightness, { -100, 100 } } +}; + GraphicsEffect::GraphicsEffect(ShaderManager::Effect effect, const std::string &name) : m_effect(effect), m_name(name) @@ -19,3 +24,13 @@ std::string GraphicsEffect::name() const { return m_name; } + + +double GraphicsEffect::clamp(double value) const +{ + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/blocks/scratch3_looks.js#L523-L538 + if(m_effect == ShaderManager::Effect::Ghost || m_effect == ShaderManager::Effect::Brightness) + return std::clamp(value, EFFECT_RANGE[m_effect].first, EFFECT_RANGE[m_effect].second); + + return value; +} diff --git a/src/graphicseffect.h b/src/graphicseffect.h index 2719fca..17a4305 100644 --- a/src/graphicseffect.h +++ b/src/graphicseffect.h @@ -17,6 +17,8 @@ class GraphicsEffect : public libscratchcpp::IGraphicsEffect ShaderManager::Effect effect() const; std::string name() const override; + double clamp(double value) const override; + private: ShaderManager::Effect m_effect = static_cast(0); std::string m_name; diff --git a/src/internal/ListMonitor.qml b/src/internal/ListMonitor.qml index 36e02d4..8fe6875 100644 --- a/src/internal/ListMonitor.qml +++ b/src/internal/ListMonitor.qml @@ -17,6 +17,8 @@ Rectangle { border.color: Qt.rgba(0.765, 0.8, 0.85, 1) radius: 5 visible: model ? model.visible : true + onWidthChanged: if(model) model.width = width + onHeightChanged: if(model) model.height = height QtObject { id: priv @@ -75,6 +77,7 @@ Rectangle { ListView { property real oldContentY readonly property int scrollBarWidth: 15 + property int itemHeight: 24 // NOTE: Hard-coded value id: listView anchors.left: parent.left @@ -85,6 +88,25 @@ Rectangle { clip: true model: root.model ? root.model.listModel : null boundsBehavior: Flickable.StopAtBounds + onContentXChanged: updateVisibleIndexRange() + onContentYChanged: updateVisibleIndexRange() + onCountChanged: updateVisibleIndexRange() + + function getVisibleIndexRange() { + if(count === 0) + return [0, 0]; + + let y1 = listView.contentY - itemHeight; + let y2 = listView.contentY + listView.height; + return [Math.max(0, Math.ceil(y1 / itemHeight)), + Math.min(Math.floor(y2 / itemHeight), count - 1)]; + } + + function updateVisibleIndexRange() { + let range = getVisibleIndexRange(); + root.model.minIndex = range[0]; + root.model.maxIndex = range[1]; + } ScrollBar.vertical: ScrollBar { id: scrollBar diff --git a/src/internal/ValueMonitor.qml b/src/internal/ValueMonitor.qml index 74dd34f..5a46723 100644 --- a/src/internal/ValueMonitor.qml +++ b/src/internal/ValueMonitor.qml @@ -15,11 +15,13 @@ Rectangle { width: layout.implicitWidth + priv.horizontalMargins * 2 height: layout.implicitHeight + priv.verticalMargins * 2 visible: model ? model.visible : true + onWidthChanged: if(model) model.width = width + onHeightChanged: if(model) model.height = height QtObject { id: priv readonly property int horizontalMargins: 9 - readonly property double verticalMargins: 2 + readonly property double verticalMargins: 2.5 readonly property color bgColor: Qt.rgba(0.9, 0.94, 1, 1) } diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 7925cd0..662e95b 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -92,7 +92,8 @@ class IRenderedTarget : public QNanoQuickItem virtual QRgb colorAtScratchPoint(double x, double y) const = 0; virtual bool touchingClones(const std::vector &clones) const = 0; - virtual bool touchingColor(const libscratchcpp::Value &color) const = 0; + virtual bool touchingColor(libscratchcpp::Rgb color) const = 0; + virtual bool touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const = 0; }; } // namespace scratchcpprender diff --git a/src/listmonitorlistmodel.cpp b/src/listmonitorlistmodel.cpp index 3cd7fd7..e7410eb 100644 --- a/src/listmonitorlistmodel.cpp +++ b/src/listmonitorlistmodel.cpp @@ -11,11 +11,14 @@ ListMonitorListModel::ListMonitorListModel(QObject *parent) : { } -void ListMonitorListModel::setList(libscratchcpp::List *list) +void ListMonitorListModel::setList(libscratchcpp::List *list, size_t minVisibleIndex, size_t maxVisibleIndex) { if (!list) return; + m_minIndex = minVisibleIndex; + m_maxIndex = maxVisibleIndex; + // Initial load if (m_list != list) { beginResetModel(); @@ -25,10 +28,8 @@ void ListMonitorListModel::setList(libscratchcpp::List *list) return; } - // Notify about changed items - int count = std::min(m_oldRowCount, static_cast(m_list->size())); - - for (int i = 0; i < count; i++) + // Update visible items + for (size_t i = minVisibleIndex; i <= maxVisibleIndex; i++) emit dataChanged(index(i), index(i)); // Notify about new items (at the end of the list) @@ -54,10 +55,10 @@ int ListMonitorListModel::rowCount(const QModelIndex &parent) const QVariant ListMonitorListModel::data(const QModelIndex &index, int role) const { - if (!m_list || index.row() < 0 || index.row() >= m_list->size()) - return QVariant(); + if (!m_list || index.row() < m_minIndex || index.row() > m_maxIndex) + return ""; - return QString::fromStdString((*m_list)[index.row()].toString()); + return QString::fromStdString(libscratchcpp::Value((*m_list)[index.row()]).toString()); } QHash ListMonitorListModel::roleNames() const diff --git a/src/listmonitorlistmodel.h b/src/listmonitorlistmodel.h index cb0a8ce..26359a5 100644 --- a/src/listmonitorlistmodel.h +++ b/src/listmonitorlistmodel.h @@ -19,7 +19,7 @@ class ListMonitorListModel : public QAbstractListModel public: explicit ListMonitorListModel(QObject *parent = nullptr); - void setList(libscratchcpp::List *list); + void setList(libscratchcpp::List *list, size_t minVisibleIndex, size_t maxVisibleIndex); int rowCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; @@ -27,7 +27,9 @@ class ListMonitorListModel : public QAbstractListModel private: libscratchcpp::List *m_list = nullptr; - int m_oldRowCount = 0; + size_t m_oldRowCount = 0; + size_t m_minIndex = 0; + size_t m_maxIndex = 0; }; } // namespace scratchcpprender diff --git a/src/listmonitormodel.cpp b/src/listmonitormodel.cpp index 2a42315..5c09847 100644 --- a/src/listmonitormodel.cpp +++ b/src/listmonitormodel.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -#include +#include #include "listmonitormodel.h" #include "listmonitorlistmodel.h" @@ -12,19 +12,16 @@ ListMonitorModel::ListMonitorModel(QObject *parent) : { } -ListMonitorModel::ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent) : - MonitorModel(section, parent) +ListMonitorModel::ListMonitorModel(libscratchcpp::IExtension *extension, QObject *parent) : + MonitorModel(extension, parent) { m_listModel = new ListMonitorListModel(this); } -void ListMonitorModel::onValueChanged(const libscratchcpp::VirtualMachine *vm) +void ListMonitorModel::onValueChanged(const libscratchcpp::Value &value) { - if (vm->registerCount() == 1) { - long index = vm->getInput(0, 1)->toLong(); - libscratchcpp::List *list = vm->lists()[index]; - m_listModel->setList(list); - } + libscratchcpp::List *list = (libscratchcpp::List *)value.toPointer(); + m_listModel->setList(list, m_minIndex, m_maxIndex); } MonitorModel::Type ListMonitorModel::type() const @@ -36,3 +33,31 @@ ListMonitorListModel *ListMonitorModel::listModel() const { return m_listModel; } + +size_t ListMonitorModel::minIndex() const +{ + return m_minIndex; +} + +void ListMonitorModel::setMinIndex(size_t newMinIndex) +{ + if (m_minIndex == newMinIndex) + return; + + m_minIndex = newMinIndex; + emit minIndexChanged(); +} + +size_t ListMonitorModel::maxIndex() const +{ + return m_maxIndex; +} + +void ListMonitorModel::setMaxIndex(size_t newMaxIndex) +{ + if (m_maxIndex == newMaxIndex) + return; + + m_maxIndex = newMaxIndex; + emit maxIndexChanged(); +} diff --git a/src/listmonitormodel.h b/src/listmonitormodel.h index 45a6e8b..3452b7e 100644 --- a/src/listmonitormodel.h +++ b/src/listmonitormodel.h @@ -18,23 +18,35 @@ class ListMonitorModel : public MonitorModel Q_OBJECT QML_ELEMENT Q_PROPERTY(ListMonitorListModel *listModel READ listModel NOTIFY listModelChanged) + Q_PROPERTY(size_t minIndex READ minIndex WRITE setMinIndex NOTIFY minIndexChanged FINAL); + Q_PROPERTY(size_t maxIndex READ maxIndex WRITE setMaxIndex NOTIFY maxIndexChanged FINAL) public: ListMonitorModel(QObject *parent = nullptr); - ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr); + ListMonitorModel(libscratchcpp::IExtension *extension, QObject *parent = nullptr); - void onValueChanged(const libscratchcpp::VirtualMachine *vm) override; + void onValueChanged(const libscratchcpp::Value &value) override; Type type() const override; ListMonitorListModel *listModel() const; + size_t minIndex() const; + void setMinIndex(size_t newMinIndex); + + size_t maxIndex() const; + void setMaxIndex(size_t newMaxIndex); + signals: void colorChanged(); void listModelChanged(); + void minIndexChanged(); + void maxIndexChanged(); private: ListMonitorListModel *m_listModel = nullptr; + size_t m_minIndex = 0; + size_t m_maxIndex = 0; }; } // namespace scratchcpprender diff --git a/src/monitormodel.cpp b/src/monitormodel.cpp index 9554276..60bd8db 100644 --- a/src/monitormodel.cpp +++ b/src/monitormodel.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include "monitormodel.h" @@ -13,14 +13,14 @@ MonitorModel::MonitorModel(QObject *parent) : { } -MonitorModel::MonitorModel(libscratchcpp::IBlockSection *section, QObject *parent) : +MonitorModel::MonitorModel(libscratchcpp::IExtension *extension, QObject *parent) : QObject(parent) { - if (!section) + if (!extension) return; - // TODO: Get the color from the block section - std::string name = section->name(); + // TODO: Get the color from the extension + std::string name = extension->name(); if (name == "Motion") m_color = QColor::fromString("#4C97FF"); else if (name == "Looks") @@ -49,7 +49,7 @@ QString MonitorModel::name() const bool MonitorModel::visible() const { if (m_monitor) - return m_monitor->visible(); + return m_monitor->visible() && !m_monitor->needsAutoPosition(); else return false; } @@ -69,6 +69,18 @@ void MonitorModel::onVisibleChanged(bool visible) emit visibleChanged(); } +void MonitorModel::onXChanged(int x) +{ + emit xChanged(); + emit visibleChanged(); +} + +void MonitorModel::onYChanged(int y) +{ + emit yChanged(); + emit visibleChanged(); +} + libscratchcpp::Monitor *MonitorModel::monitor() const { return m_monitor; diff --git a/src/monitormodel.h b/src/monitormodel.h index 329a284..bf1120b 100644 --- a/src/monitormodel.h +++ b/src/monitormodel.h @@ -10,7 +10,7 @@ namespace libscratchcpp { -class IBlockSection; +class IExtension; } @@ -43,12 +43,14 @@ class MonitorModel Q_ENUM(Type) MonitorModel(QObject *parent = nullptr); - MonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr); + MonitorModel(libscratchcpp::IExtension *extension, QObject *parent = nullptr); void init(libscratchcpp::Monitor *monitor) override final; - virtual void onValueChanged(const libscratchcpp::VirtualMachine *vm) override { } + virtual void onValueChanged(const libscratchcpp::Value &value) override { } void onVisibleChanged(bool visible) override final; + void onXChanged(int x) override final; + void onYChanged(int y) override final; libscratchcpp::Monitor *monitor() const; diff --git a/src/mouseeventhandler.cpp b/src/mouseeventhandler.cpp index 18876ea..e1dde78 100644 --- a/src/mouseeventhandler.cpp +++ b/src/mouseeventhandler.cpp @@ -165,7 +165,7 @@ void MouseEventHandler::forwardPointEvent(QSinglePointEvent *event, QQuickItem * // contains() expects position in the item's coordinate system QPointF localPos = sprite->mapFromScene(event->scenePosition()); - if (sprite->contains(localPos)) { + if (static_cast(sprite->scratchTarget())->visible() && sprite->contains(localPos)) { hoveredItem = sprite; break; } diff --git a/src/penlayer.cpp b/src/penlayer.cpp index 031d504..b93cb30 100644 --- a/src/penlayer.cpp +++ b/src/penlayer.cpp @@ -11,8 +11,17 @@ using namespace scratchcpprender; +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 + std::unordered_map PenLayer::m_projectPenLayers; +// TODO: Move this to a separate class +template +short sgn(T x) +{ + return (T(0) < x) - (x < T(0)); +} + PenLayer::PenLayer(QNanoQuickItem *parent) : IPenLayer(parent) { @@ -24,10 +33,13 @@ PenLayer::~PenLayer() if (m_engine) m_projectPenLayers.erase(m_engine); - if (m_blitter.isCreated()) { + if (m_vao != 0) { // Delete vertex array and buffer m_glF->glDeleteVertexArrays(1, &m_vao); m_glF->glDeleteBuffers(1, &m_vbo); + + // Delete stamp FBO + m_glF->glDeleteFramebuffers(1, &m_stampFbo); } } @@ -56,9 +68,15 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_engine = newEngine; - if (m_engine && QOpenGLContext::currentContext()) { + if (!m_glCtx) { + m_glCtx = QOpenGLContext::currentContext(); + + if (m_glCtx) + m_surface = m_glCtx->surface(); + } + + if (m_engine && m_glCtx) { m_projectPenLayers[m_engine] = this; - createFbo(); if (!m_painter) m_painter = std::make_unique(); @@ -68,13 +86,11 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_glF->initializeOpenGLFunctions(); } - if (!m_blitter.isCreated()) { - m_blitter.create(); + refresh(); + if (m_vao == 0) { // Set up VBO and VAO - float vertices[] = { - -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, - }; + float vertices[] = { -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f }; m_glF->glGenVertexArrays(1, &m_vao); m_glF->glGenBuffers(1, &m_vbo); @@ -94,6 +110,9 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Create stamp FBO + m_glF->glGenFramebuffers(1, &m_stampFbo); } clear(); @@ -113,8 +132,8 @@ void PenLayer::setHqPen(bool newHqPen) return; m_hqPen = newHqPen; - createFbo(); emit hqPenChanged(); + refresh(); } void scratchcpprender::PenLayer::clear() @@ -195,98 +214,89 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do update(); } -/* - * A brief description of how stamping is implemented: - * 1. Get rotation, size and coordinates and translate them. - * 2. Draw the texture onto a temporary texture using shaders. - * 3. Blit the resulting texture to a FBO with a square texture (required for rotation). - * 4. Blit the resulting texture to the pen layer using QOpenGLTextureBlitter with transform. - * - * If you think this is too complicated, contributions are welcome! - */ void PenLayer::stamp(IRenderedTarget *target) { - if (!target || !m_fbo || !m_texture.isValid() || !m_blitter.isCreated()) + if (!target || !m_fbo || !m_texture.isValid() || m_vao == 0 || m_vbo == 0) + return; + + const float stageWidth = m_engine->stageWidth() * m_scale; + const float stageHeight = m_engine->stageHeight() * m_scale; + + libscratchcpp::Rect bounds = target->getFastBounds(); + bounds.snapToInt(); + + if (!bounds.intersects(libscratchcpp::Rect(-stageWidth / 2, stageHeight / 2, stageWidth / 2, -stageHeight / 2))) return; - double x = 0; - double y = 0; - double angle = 0; - double scale = 1; - bool mirror = false; - std::shared_ptr costume; + float x = 0; + float y = 0; + float angle = 180; + float scaleX = 1; + float scaleY = 1; SpriteModel *spriteModel = target->spriteModel(); if (spriteModel) { libscratchcpp::Sprite *sprite = spriteModel->sprite(); - x = sprite->x(); - y = sprite->y(); switch (sprite->rotationStyle()) { case libscratchcpp::Sprite::RotationStyle::AllAround: - angle = 90 - sprite->direction(); + angle = 270 - sprite->direction(); break; case libscratchcpp::Sprite::RotationStyle::LeftRight: - mirror = (sprite->direction() < 0); + scaleX = sgn(sprite->direction()); break; default: break; } - scale = sprite->size() / 100; - costume = sprite->currentCostume(); - } else - costume = target->stageModel()->stage()->currentCostume(); - - // Apply scale (HQ pen) - scale *= m_scale; + scaleY = sprite->size() / 100; + scaleX *= scaleY; + } - const double bitmapRes = costume->bitmapResolution(); - const double centerX = costume->rotationCenterX() / bitmapRes; - const double centerY = costume->rotationCenterY() / bitmapRes; + scaleX *= m_scale; + scaleY *= m_scale; const Texture &texture = target->cpuTexture(); if (!texture.isValid()) return; - const double textureScale = texture.width() / static_cast(target->costumeWidth()); - - // Apply scale (HQ pen) - x *= m_scale; - y *= m_scale; - - // Translate the coordinates - x = std::floor(x + m_texture.width() / 2.0); - y = std::floor(-y + m_texture.height() / 2.0); - + const float textureScale = texture.width() / static_cast(target->costumeWidth()); + const float skinWidth = texture.width(); + const float skinHeight = texture.height(); + + // Projection matrix + QMatrix4x4 projectionMatrix; + const float aspectRatio = skinHeight / skinWidth; + projectionMatrix.ortho(1.0f, -1.0f, aspectRatio, -aspectRatio, 0.1f, 0.0f); + projectionMatrix.scale(skinWidth / bounds.width() / m_scale, skinHeight / bounds.height() / m_scale); + + // Model matrix + // TODO: This should be calculated and cached by targets + QMatrix4x4 modelMatrix; + modelMatrix.rotate(angle, 0, 0, 1); + modelMatrix.scale(scaleX / textureScale, aspectRatio * scaleY / textureScale); m_glF->glDisable(GL_SCISSOR_TEST); - - // For some reason nothing is rendered without this - // TODO: Find out why this is happening - m_painter->beginFrame(m_fbo->width(), m_fbo->height()); - m_painter->stroke(); - m_painter->endFrame(); - - // Create a temporary FBO for graphic effects - QOpenGLFramebufferObject tmpFbo(texture.size()); - m_painter->beginFrame(tmpFbo.width(), tmpFbo.height()); + m_glF->glDisable(GL_DEPTH_TEST); + m_glF->glEnable(GL_BLEND); + m_glF->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Create a FBO for the current texture - unsigned int fbo; - m_glF->glGenFramebuffers(1, &fbo); - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_stampFbo); m_glF->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); if (m_glF->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { qWarning() << "error: framebuffer incomplete (stamp " + target->scratchTarget()->name() + ")"; - m_glF->glDeleteFramebuffers(1, &fbo); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); return; } + // Set viewport + m_glF->glViewport((stageWidth / 2) + bounds.left() * m_scale, (stageHeight / 2) + bounds.bottom() * m_scale, bounds.width() * m_scale, bounds.height() * m_scale); + // Get the shader program for the current set of effects ShaderManager *shaderManager = ShaderManager::instance(); @@ -298,68 +308,65 @@ void PenLayer::stamp(IRenderedTarget *target) m_glF->glBindBuffer(GL_ARRAY_BUFFER, m_vbo); // Render to the target framebuffer - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, tmpFbo.handle()); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_fbo->handle()); shaderProgram->bind(); m_glF->glBindVertexArray(m_vao); m_glF->glActiveTexture(GL_TEXTURE0); m_glF->glBindTexture(GL_TEXTURE_2D, texture.handle()); - shaderManager->setUniforms(shaderProgram, 0, effects); // set texture and effect uniforms - m_glF->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - m_painter->endFrame(); - - // Resize to square (for rotation) - const double dim = std::max(tmpFbo.width(), tmpFbo.height()); - QOpenGLFramebufferObject resizeFbo(dim, dim); - resizeFbo.bind(); - m_painter->beginFrame(dim, dim); - - const QRect resizeRect(QPoint(0, 0), tmpFbo.size()); - const QMatrix4x4 matrix = QOpenGLTextureBlitter::targetTransform(resizeRect, QRect(QPoint(0, 0), resizeFbo.size())); - m_glF->glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - m_glF->glClear(GL_COLOR_BUFFER_BIT); - m_blitter.bind(); - m_blitter.blit(tmpFbo.texture(), matrix, QOpenGLTextureBlitter::OriginBottomLeft); - m_blitter.release(); - - m_painter->endFrame(); - resizeFbo.release(); + shaderManager->setUniforms(shaderProgram, 0, texture.size(), effects); // set texture and effect uniforms + shaderProgram->setUniformValue("u_projectionMatrix", projectionMatrix); + shaderProgram->setUniformValue("u_modelMatrix", modelMatrix); + m_glF->glDrawArrays(GL_TRIANGLES, 0, 6); // Cleanup shaderProgram->release(); m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); - m_glF->glDeleteFramebuffers(1, &fbo); - - // Transform - const double width = resizeFbo.width() / textureScale; - const double height = resizeFbo.height() / textureScale; - QRectF targetRect(QPoint(x, y), QSizeF(width, height)); - QTransform transform = QOpenGLTextureBlitter::targetTransform(targetRect, QRect(QPoint(centerX, centerY), m_fbo->size())).toTransform(); - const double dx = 2 * (centerX - width / 2.0) / width; - const double dy = -2 * (centerY - height / 2.0) / height; - transform.translate(dx, dy); - transform.rotate(angle); - transform.scale(scale * (mirror ? -1 : 1), scale); - transform.translate(-dx, -dy); - - // Blit - m_fbo->bind(); - m_painter->beginFrame(m_fbo->width(), m_fbo->height()); - m_blitter.bind(); - m_blitter.blit(resizeFbo.texture(), transform, QOpenGLTextureBlitter::OriginBottomLeft); - m_blitter.release(); - m_painter->endFrame(); - m_fbo->release(); m_glF->glEnable(GL_SCISSOR_TEST); + m_glF->glEnable(GL_DEPTH_TEST); m_textureDirty = true; m_boundsDirty = true; update(); } +void PenLayer::refresh() +{ + if (!m_glCtx || !m_surface || !m_engine || !m_glF) + return; + + QOpenGLContext *oldCtx = QOpenGLContext::currentContext(); + QSurface *oldSurface = oldCtx->surface(); + + if (oldCtx != m_glCtx) { + oldCtx->doneCurrent(); + m_glCtx->makeCurrent(m_surface); + } + + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject *newFbo = new QOpenGLFramebufferObject(width(), height(), fboFormat); + Q_ASSERT(newFbo->isValid()); + + if (m_fbo) { + m_glF->glDisable(GL_SCISSOR_TEST); + QOpenGLFramebufferObject::blitFramebuffer(newFbo, m_fbo.get()); + m_glF->glEnable(GL_SCISSOR_TEST); + } + + m_fbo.reset(newFbo); + m_texture = Texture(m_fbo->texture(), m_fbo->size()); + m_scale = width() / m_engine->stageWidth(); + + if (oldCtx != m_glCtx) { + m_glCtx->doneCurrent(); + oldCtx->makeCurrent(oldSurface); + } +} + QOpenGLFramebufferObject *PenLayer::framebufferObject() const { return m_fbo.get(); @@ -412,7 +419,8 @@ const libscratchcpp::Rect &PenLayer::getBounds() const double bottom = std::numeric_limits::infinity(); const double width = m_texture.width(); const double height = m_texture.height(); - const std::vector &points = m_textureManager.getTextureConvexHullPoints(m_texture); + std::vector points; + m_textureManager.getTextureConvexHullPoints(m_texture, QSize(), ShaderManager::Effect::NoEffect, {}, points); if (points.empty()) { m_bounds = libscratchcpp::Rect(); @@ -462,36 +470,21 @@ void PenLayer::addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer) QNanoQuickItemPainter *PenLayer::createItemPainter() const { + m_glCtx = QOpenGLContext::currentContext(); + Q_ASSERT(m_glCtx); + m_surface = m_glCtx->surface(); + Q_ASSERT(m_surface); return new PenLayerPainter; } void PenLayer::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) { if (m_hqPen && newGeometry != oldGeometry) - createFbo(); + refresh(); QNanoQuickItem::geometryChange(newGeometry, oldGeometry); } -void PenLayer::createFbo() -{ - if (!QOpenGLContext::currentContext() || !m_engine) - return; - - QOpenGLFramebufferObjectFormat fboFormat; - fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - QOpenGLFramebufferObject *newFbo = new QOpenGLFramebufferObject(width(), height(), fboFormat); - Q_ASSERT(newFbo->isValid()); - - if (m_fbo) - QOpenGLFramebufferObject::blitFramebuffer(newFbo, m_fbo.get()); - - m_fbo.reset(newFbo); - m_texture = Texture(m_fbo->texture(), m_fbo->size()); - m_scale = width() / m_engine->stageWidth(); -} - void PenLayer::updateTexture() { if (!m_fbo) diff --git a/src/penlayer.h b/src/penlayer.h index 2f93a9c..53c614c 100644 --- a/src/penlayer.h +++ b/src/penlayer.h @@ -39,6 +39,8 @@ class PenLayer : public IPenLayer void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) override; void stamp(IRenderedTarget *target) override; + Q_INVOKABLE void refresh(); + QOpenGLFramebufferObject *framebufferObject() const override; QRgb colorAtScratchPoint(double x, double y) const override; @@ -56,14 +58,16 @@ class PenLayer : public IPenLayer void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; private: - void createFbo(); void updateTexture(); static std::unordered_map m_projectPenLayers; + static inline GLuint m_stampFbo = 0; bool m_antialiasingEnabled = true; libscratchcpp::IEngine *m_engine = nullptr; bool m_hqPen = false; std::unique_ptr m_fbo; + mutable QOpenGLContext *m_glCtx = nullptr; + mutable QSurface *m_surface = nullptr; double m_scale = 1; std::unique_ptr m_painter; std::unique_ptr m_glF; @@ -72,7 +76,6 @@ class PenLayer : public IPenLayer mutable CpuTextureManager m_textureManager; mutable bool m_boundsDirty = true; mutable libscratchcpp::Rect m_bounds; - QOpenGLTextureBlitter m_blitter; GLuint m_vbo = 0; GLuint m_vao = 0; }; diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 3090487..1453620 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -12,7 +12,7 @@ #include "valuemonitormodel.h" #include "listmonitormodel.h" #include "renderedtarget.h" -#include "blocks/penextension.h" +// #include "blocks/penblocks.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -32,18 +32,18 @@ ProjectLoader::ProjectLoader(QObject *parent) : } }); + connect(qApp, &QCoreApplication::aboutToQuit, this, &ProjectLoader::clear); + initTimer(); + m_renderTimer.start(); - // Register pen blocks - ScratchConfiguration::registerExtension(std::make_shared()); + // TODO: Register pen blocks + // ScratchConfiguration::registerExtension(std::make_shared()); } ProjectLoader::~ProjectLoader() { - m_stopLoading = true; - - if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + stopLoading(); for (SpriteModel *sprite : m_sprites) sprite->deleteLater(); @@ -56,56 +56,21 @@ const QString &ProjectLoader::fileName() const void ProjectLoader::setFileName(const QString &newFileName) { - if (m_loadThread.isRunning()) + if (m_loadThread.isRunning()) { + stopLoading(); m_loadThread.waitForFinished(); + QCoreApplication::processEvents(); + } if (newFileName.isEmpty()) return; m_fileName = newFileName; - // Stop the project - if (m_engine) - m_engine->stop(); - - // Reset stage model - m_stage.init(nullptr); - - if (m_stage.renderedTarget()) - m_stage.renderedTarget()->update(); - - // Delete old sprites - for (SpriteModel *sprite : m_sprites) - sprite->deleteLater(); - - m_sprites.clear(); - emit spritesChanged(); - - // Delete old clones - for (SpriteModel *clone : m_clones) - deleteCloneObject(clone); - - m_clones.clear(); - emit clonesChanged(); - - // Delete old monitors - for (MonitorModel *monitor : m_monitors) { - emit monitorRemoved(monitor); - monitor->deleteLater(); - } - - m_monitors.clear(); - emit monitorsChanged(); - - // Clear the engine - if (m_engine) - m_engine->clear(); - - m_engine = nullptr; - emit engineChanged(); + clear(); m_project.setFileName(m_fileName.toStdString()); - m_loadStatus = false; + m_loadStatus = LoadStatus::Loading; // TODO: Do not set these to 0 after libscratchcpp starts doing it itself m_downloadedAssets = 0; @@ -120,14 +85,33 @@ void ProjectLoader::setFileName(const QString &newFileName) m_loadThread = QtConcurrent::run(&callLoad, this); } -bool ProjectLoader::loadStatus() const +ProjectLoader::LoadStatus ProjectLoader::loadStatus() const { if (m_loadThread.isRunning()) - return false; + return LoadStatus::Loading; return m_loadStatus; } +void ProjectLoader::stopLoading() +{ + if (m_loadThread.isRunning()) { + m_project.stopLoading(); + m_stopLoading = true; + m_loadThread.waitForFinished(); + } +} + +bool ProjectLoader::running() const +{ + return m_running; +} + +int ProjectLoader::renderFps() const +{ + return m_renderFps; +} + IEngine *ProjectLoader::engine() const { if (m_loadThread.isRunning()) @@ -145,15 +129,17 @@ void ProjectLoader::setEngine(libscratchcpp::IEngine *engine) StageModel *ProjectLoader::stage() { if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + return nullptr; return &m_stage; } QQmlListProperty ProjectLoader::sprites() { - if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + if (m_loadThread.isRunning()) { + m_emptySpriteList.clear(); + return QQmlListProperty(this, &m_emptySpriteList); + } return QQmlListProperty(this, &m_sprites); } @@ -183,12 +169,17 @@ const QList &ProjectLoader::monitorList() const return m_monitors; } +const QStringList &ProjectLoader::unsupportedBlocks() const +{ + return m_unsupportedBlocks; +} + void ProjectLoader::start() { if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + return; - if (m_loadStatus) { + if (m_loadStatus == LoadStatus::Loaded) { Q_ASSERT(m_engine); m_engine->start(); } @@ -197,9 +188,9 @@ void ProjectLoader::start() void ProjectLoader::stop() { if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + return; - if (m_loadStatus) { + if (m_loadStatus == LoadStatus::Loaded) { Q_ASSERT(m_engine); m_engine->stop(); } @@ -216,9 +207,54 @@ void ProjectLoader::timerEvent(QTimerEvent *event) if (m_loadThread.isRunning()) return; - if (m_engine) + if (m_engine) { + QOpenGLContext *oldCtx = QOpenGLContext::currentContext(); + QSurface *oldSurface = nullptr; + + if (!m_glCtx) + m_glCtx = oldCtx; + + if (m_glCtx) { + if (!m_surface) + m_surface = m_glCtx->surface(); + + oldSurface = oldCtx->surface(); + + if (oldCtx != m_glCtx) { + oldCtx->doneCurrent(); + m_glCtx->makeCurrent(m_surface); + } + } + + for (Monitor *monitor : m_unpositionedMonitors) + monitor->autoPosition(m_engine->monitors()); + + m_unpositionedMonitors.clear(); + m_engine->step(); + if (m_running != m_engine->isRunning()) { + m_running = !m_running; + emit runningChanged(); + } + + // FPS counter + if (m_renderTimer.elapsed() >= 1000) { + m_renderFps = m_renderFpsCounter; + m_renderFpsCounter = 0; + emit renderFpsChanged(); + m_renderTimer.restart(); + } else + m_renderFpsCounter++; + + if (m_glCtx) { + if (oldCtx != m_glCtx) { + m_glCtx->doneCurrent(); + oldCtx->makeCurrent(oldSurface); + } + } + } + event->accept(); } @@ -227,14 +263,63 @@ void ProjectLoader::callLoad(ProjectLoader *loader) loader->load(); } +void ProjectLoader::clear() +{ + // Stop the project + if (m_engine) + m_engine->stop(); + + // Reset stage model + m_stage.init(nullptr); + + if (m_stage.renderedTarget()) + m_stage.renderedTarget()->update(); + + // Delete old sprites + for (SpriteModel *sprite : m_sprites) + sprite->deleteLater(); + + m_sprites.clear(); + emit spritesChanged(); + + // Delete old clones + for (SpriteModel *clone : m_clones) + deleteCloneObject(clone); + + m_clones.clear(); + emit clonesChanged(); + + // Delete old monitors + for (MonitorModel *monitor : m_monitors) { + emit monitorRemoved(monitor); + monitor->deleteLater(); + } + + m_monitors.clear(); + emit monitorsChanged(); + + // Clear the engine + if (m_engine) + m_engine->clear(); + + m_oldEngine = m_engine; + m_engine = nullptr; + emit engineChanged(); +} + void ProjectLoader::load() { - m_loadStatus = m_project.load(); + m_unpositionedMonitors.clear(); + m_loadStatus = m_project.load() ? LoadStatus::Loaded : LoadStatus::Failed; m_engineMutex.lock(); m_engine = m_project.engine().get(); if (!m_engine || m_stopLoading) { m_engineMutex.unlock(); + + if (m_stopLoading) + m_loadStatus = LoadStatus::Aborted; + emit fileNameChanged(); emit loadStatusChanged(); emit loadingFinished(); @@ -251,12 +336,14 @@ void ProjectLoader::load() m_engine->setSpriteFencingEnabled(m_spriteFencing); m_engine->setGlobalVolume(m_mute ? 0 : 100); - m_engine->aboutToRender().connect(&ProjectLoader::redraw, this); - m_engine->monitorAdded().connect(&ProjectLoader::addMonitor, this); - m_engine->monitorRemoved().connect(&ProjectLoader::removeMonitor, this); + if (m_engine != m_oldEngine) { + m_engine->aboutToRender().connect(&ProjectLoader::redraw, this); + m_engine->monitorAdded().connect(&ProjectLoader::addMonitor, this); + m_engine->monitorRemoved().connect(&ProjectLoader::removeMonitor, this); - m_engine->questionAsked().connect([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); - m_engine->questionAborted().connect([this]() { emit questionAborted(); }); + m_engine->questionAsked().connect([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); + m_engine->questionAborted().connect([this]() { emit questionAborted(); }); + } // Load targets const auto &targets = m_engine->targets(); @@ -281,6 +368,7 @@ void ProjectLoader::load() if (m_stopLoading) { m_engineMutex.unlock(); + m_loadStatus = LoadStatus::Aborted; emit fileNameChanged(); emit loadStatusChanged(); emit loadingFinished(); @@ -289,6 +377,15 @@ void ProjectLoader::load() return; } + // Get unsupported blocks + const auto &unsupportedBlocks = m_engine->unsupportedBlocks(); + m_unsupportedBlocks.clear(); + + for (const std::string &opcode : unsupportedBlocks) + m_unsupportedBlocks.push_back(QString::fromStdString(opcode)); + + emit unsupportedBlocksChanged(); + m_engineMutex.unlock(); emit loadStatusChanged(); @@ -306,7 +403,7 @@ void ProjectLoader::initTimer() void ProjectLoader::redraw() { if (m_loadThread.isRunning()) - m_loadThread.waitForFinished(); + return; auto stage = m_stage.renderedTarget(); @@ -356,26 +453,31 @@ void ProjectLoader::deleteClone(SpriteModel *model) void ProjectLoader::addMonitor(Monitor *monitor) { - auto section = monitor->blockSection(); + auto extension = monitor->extension(); - if (!section) + if (!extension) return; MonitorModel *model = nullptr; switch (monitor->mode()) { case Monitor::Mode::List: - model = new ListMonitorModel(section.get()); + model = new ListMonitorModel(extension); break; default: - model = new ValueMonitorModel(section.get()); + model = new ValueMonitorModel(extension); break; } if (!model) return; + if (monitor->needsAutoPosition()) { + if (std::find(m_unpositionedMonitors.begin(), m_unpositionedMonitors.end(), monitor) == m_unpositionedMonitors.end()) + m_unpositionedMonitors.push_back(monitor); + } + model->moveToThread(qApp->thread()); model->setParent(this); monitor->setInterface(model); diff --git a/src/projectloader.h b/src/projectloader.h index 8c949c9..249be57 100644 --- a/src/projectloader.h +++ b/src/projectloader.h @@ -12,6 +12,9 @@ Q_MOC_INCLUDE("spritemodel.h"); Q_MOC_INCLUDE("monitormodel.h"); +class QSurface; +class QOpenGLContext; + namespace scratchcpprender { @@ -23,12 +26,15 @@ class ProjectLoader : public QObject Q_OBJECT QML_ELEMENT Q_PROPERTY(QString fileName READ fileName WRITE setFileName NOTIFY fileNameChanged) - Q_PROPERTY(bool loadStatus READ loadStatus NOTIFY loadStatusChanged) + Q_PROPERTY(LoadStatus loadStatus READ loadStatus NOTIFY loadStatusChanged) + Q_PROPERTY(bool running READ running NOTIFY runningChanged) + Q_PROPERTY(int renderFps READ renderFps NOTIFY renderFpsChanged FINAL) Q_PROPERTY(libscratchcpp::IEngine *engine READ engine NOTIFY engineChanged) Q_PROPERTY(StageModel *stage READ stage NOTIFY stageChanged) Q_PROPERTY(QQmlListProperty sprites READ sprites NOTIFY spritesChanged) Q_PROPERTY(QQmlListProperty clones READ clones NOTIFY clonesChanged) Q_PROPERTY(QQmlListProperty monitors READ monitors NOTIFY monitorsChanged) + Q_PROPERTY(QStringList unsupportedBlocks READ unsupportedBlocks NOTIFY unsupportedBlocksChanged) Q_PROPERTY(double fps READ fps WRITE setFps NOTIFY fpsChanged) Q_PROPERTY(bool turboMode READ turboMode WRITE setTurboMode NOTIFY turboModeChanged) Q_PROPERTY(unsigned int stageWidth READ stageWidth WRITE setStageWidth NOTIFY stageWidthChanged) @@ -40,13 +46,27 @@ class ProjectLoader : public QObject Q_PROPERTY(unsigned int assetCount READ assetCount NOTIFY assetCountChanged) public: + enum class LoadStatus + { + Idle, + Loading, + Loaded, + Failed, + Aborted + }; + + Q_ENUM(LoadStatus) + explicit ProjectLoader(QObject *parent = nullptr); ~ProjectLoader(); const QString &fileName() const; void setFileName(const QString &newFileName); - bool loadStatus() const; + LoadStatus loadStatus() const; + Q_INVOKABLE void stopLoading(); + + bool running() const; libscratchcpp::IEngine *engine() const; void setEngine(libscratchcpp::IEngine *engine); @@ -62,6 +82,8 @@ class ProjectLoader : public QObject QQmlListProperty monitors(); const QList &monitorList() const; + const QStringList &unsupportedBlocks() const; + Q_INVOKABLE void start(); Q_INVOKABLE void stop(); @@ -92,15 +114,20 @@ class ProjectLoader : public QObject unsigned int assetCount() const; + int renderFps() const; + signals: void fileNameChanged(); void loadStatusChanged(); void loadingFinished(); + void runningChanged(); + void renderFpsChanged(); void engineChanged(); void stageChanged(); void spritesChanged(); void clonesChanged(); void monitorsChanged(); + void unsupportedBlocksChanged(); void fpsChanged(); void turboModeChanged(); void stageWidthChanged(); @@ -122,6 +149,7 @@ class ProjectLoader : public QObject private: static void callLoad(ProjectLoader *loader); + void clear(); void load(); void initTimer(); void redraw(); @@ -135,13 +163,21 @@ class ProjectLoader : public QObject QString m_fileName; QFuture m_loadThread; libscratchcpp::Project m_project; + bool m_running = false; + QElapsedTimer m_renderTimer; + int m_renderFps = 0; + int m_renderFpsCounter = 0; libscratchcpp::IEngine *m_engine = nullptr; + libscratchcpp::IEngine *m_oldEngine = nullptr; QMutex m_engineMutex; - bool m_loadStatus = false; + LoadStatus m_loadStatus = LoadStatus::Idle; StageModel m_stage; QList m_sprites; QList m_clones; + QList m_emptySpriteList; QList m_monitors; + std::vector m_unpositionedMonitors; + QStringList m_unsupportedBlocks; double m_fps = 30; bool m_turboMode = false; unsigned int m_stageWidth = 480; @@ -152,6 +188,8 @@ class ProjectLoader : public QObject std::atomic m_downloadedAssets = 0; std::atomic m_assetCount = 0; std::atomic m_stopLoading = false; + QOpenGLContext *m_glCtx = nullptr; + QSurface *m_surface = nullptr; }; } // namespace scratchcpprender diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 1f20b54..b2130f6 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -228,6 +228,7 @@ void RenderedTarget::setEngine(IEngine *newEngine) m_cpuTexture = Texture(); m_penLayer = PenLayer::getProjectPenLayer(m_engine); m_convexHullDirty = true; + m_transformedHullDirty = true; clearGraphicEffects(); m_hullPoints.clear(); @@ -371,31 +372,16 @@ void RenderedTarget::setHeight(qreal height) Rect RenderedTarget::getBounds() const { // https://github.com/scratchfoundation/scratch-render/blob/c3ede9c3d54769730c7b023021511e2aba167b1f/src/Rectangle.js#L33-L55 - if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.isValid()) - return Rect(m_x, m_y, m_x, m_y); - - const double textureScale = m_skin->getTextureScale(m_cpuTexture); - const double bitmapRes = m_costume->bitmapResolution(); - const double width = m_cpuTexture.width() * m_size / textureScale; - const double height = m_cpuTexture.height() * m_size / textureScale; - const double originX = m_costume->rotationCenterX() * m_size / bitmapRes - width / 2; - const double originY = -m_costume->rotationCenterY() * m_size / bitmapRes + height / 2; - const double rot = -rotation() * pi / 180; - const double sinRot = std::sin(rot); - const double cosRot = std::cos(rot); double left = std::numeric_limits::infinity(); double top = -std::numeric_limits::infinity(); double right = -std::numeric_limits::infinity(); double bottom = std::numeric_limits::infinity(); - const std::vector &points = hullPoints(); + const std::vector &points = transformedHullPoints(); for (const QPointF &point : points) { - double x = point.x() * m_size / textureScale / bitmapRes - width / 2; - double y = height / 2 - point.y() * m_size / textureScale / bitmapRes; - const QPointF transformed = transformPoint(x, y, originX, originY, sinRot, cosRot); - x = transformed.x() * (m_mirrorHorizontally ? -1 : 1); - y = transformed.y(); + const double x = point.x(); + const double y = point.y(); if (x < left) left = x; @@ -518,9 +504,6 @@ void RenderedTarget::mouseMoveEvent(QMouseEvent *event) m_dragDeltaX = m_engine->mouseX() - sprite->x(); m_dragDeltaY = m_engine->mouseY() - sprite->y(); m_mouseArea->setDraggedSprite(this); - - // Move the sprite to the front layer - m_engine->moveSpriteToFront(sprite); } } @@ -558,26 +541,32 @@ const std::unordered_map &RenderedTarget::graphic void RenderedTarget::setGraphicEffect(ShaderManager::Effect effect, double value) { bool changed = false; - auto it = m_graphicEffects.find(effect); if (value == 0) { - if (it != m_graphicEffects.cend()) { + if ((m_graphicEffectMask & effect) != 0) { changed = true; m_graphicEffects.erase(effect); + m_graphicEffectMask &= ~effect; } } else { - if (it != m_graphicEffects.cend()) - changed = it->second != value; - else + if ((m_graphicEffectMask & effect) != 0) + changed = m_graphicEffects[effect] != value; + else { changed = true; + m_graphicEffectMask |= effect; + } m_graphicEffects[effect] = value; } - if (changed) + if (changed) { update(); - // TODO: Set m_convexHullDirty to true if the effect changes shape + if (ShaderManager::effectShapeChanges(effect)) { + m_convexHullDirty = true; + m_transformedHullDirty = true; + } + } } void RenderedTarget::clearGraphicEffects() @@ -585,8 +574,16 @@ void RenderedTarget::clearGraphicEffects() if (!m_graphicEffects.empty()) update(); - // TODO: Set m_convexHullDirty to true if any of the previous effects changed shape + for (const auto &[effect, value] : m_graphicEffects) { + if (ShaderManager::effectShapeChanges(effect)) { + m_convexHullDirty = true; + m_transformedHullDirty = true; + break; + } + } + m_graphicEffects.clear(); + m_graphicEffectMask = ShaderManager::Effect::NoEffect; } const std::vector &RenderedTarget::hullPoints() const @@ -636,11 +633,7 @@ QRgb RenderedTarget::colorAtScratchPoint(double x, double y) const if ((x < 0 || x >= width) || (y < 0 || y >= height)) return qRgba(0, 0, 0, 0); - GLubyte *data = textureManager()->getTextureData(m_cpuTexture); - const int index = (y * width + x) * 4; // RGBA channels - Q_ASSERT(index >= 0 && index < width * height * 4); - // TODO: Apply graphic effects (#117) - return qRgba(data[index], data[index + 1], data[index + 2], data[index + 3]); + return textureManager()->getPointColor(m_cpuTexture, x, y, m_graphicEffectMask, m_graphicEffects); } bool RenderedTarget::touchingClones(const std::vector &clones) const @@ -675,42 +668,14 @@ bool RenderedTarget::touchingClones(const std::vector & return false; } -bool RenderedTarget::touchingColor(const Value &color) const +bool RenderedTarget::touchingColor(Rgb color) const { - // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L775-L841 - QRgb rgb = convertColor(color); - - std::vector targets; - getVisibleTargets(targets); - - QRectF myRect = touchingBounds(); - std::vector candidates; - QRectF bounds = candidatesBounds(myRect, targets, candidates); - - if (colorMatches(rgb, qRgb(255, 255, 255))) { - // The color we're checking for is the background color which spans the entire stage - bounds = myRect; - - if (bounds.isEmpty()) - return false; - } else if (candidates.empty()) { - // If not checking for the background color, we can return early if there are no candidate drawables - return false; - } - - // Loop through the points of the union - for (int y = bounds.top(); y <= bounds.bottom(); y++) { - for (int x = bounds.left(); x <= bounds.right(); x++) { - if (this->containsScratchPoint(x, y)) { - QRgb pixelColor = sampleColor3b(x, y, candidates); - - if (colorMatches(rgb, pixelColor)) - return true; - } - } - } + return touchingColor(color, false, 0); +} - return false; +bool RenderedTarget::touchingColor(Rgb color, Rgb mask) const +{ + return touchingColor(color, true, mask); } void RenderedTarget::calculatePos() @@ -718,53 +683,53 @@ void RenderedTarget::calculatePos() if (!m_skin || !m_costume || !m_engine) return; - if (isVisible() || m_stageModel) { - double stageWidth = m_engine->stageWidth(); - double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); - setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); - setTransformOriginPoint(QPointF(originX, originY)); - - // Qt ignores the transform origin point if it's (0, 0), - // so set the transform origin to top left in this case. - if (originX == 0 && originY == 0) - setTransformOrigin(QQuickItem::TopLeft); - else - setTransformOrigin(QQuickItem::Center); - } + double stageWidth = m_engine->stageWidth(); + double stageHeight = m_engine->stageHeight(); + setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); + setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); + qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + setTransformOriginPoint(QPointF(originX, originY)); + + // Qt ignores the transform origin point if it's (0, 0), + // so set the transform origin to top left in this case. + if (originX == 0 && originY == 0) + setTransformOrigin(QQuickItem::TopLeft); + else + setTransformOrigin(QQuickItem::Center); + + m_transformedHullDirty = true; } void RenderedTarget::calculateRotation() { - if (isVisible()) { - // Direction - bool oldMirrorHorizontally = m_mirrorHorizontally; - - switch (m_rotationStyle) { - case Sprite::RotationStyle::AllAround: - setRotation(m_direction - 90); - m_mirrorHorizontally = (false); + // Direction + bool oldMirrorHorizontally = m_mirrorHorizontally; - break; + switch (m_rotationStyle) { + case Sprite::RotationStyle::AllAround: + setRotation(m_direction - 90); + m_mirrorHorizontally = (false); - case Sprite::RotationStyle::LeftRight: { - setRotation(0); - m_mirrorHorizontally = (m_direction < 0); + break; - break; - } + case Sprite::RotationStyle::LeftRight: { + setRotation(0); + m_mirrorHorizontally = (m_direction < 0); - case Sprite::RotationStyle::DoNotRotate: - setRotation(0); - m_mirrorHorizontally = false; - break; + break; } - if (m_mirrorHorizontally != oldMirrorHorizontally) - emit mirrorHorizontallyChanged(); + case Sprite::RotationStyle::DoNotRotate: + setRotation(0); + m_mirrorHorizontally = false; + break; } + + if (m_mirrorHorizontally != oldMirrorHorizontally) + emit mirrorHorizontallyChanged(); + + m_transformedHullDirty = true; } void RenderedTarget::calculateSize() @@ -780,6 +745,8 @@ void RenderedTarget::calculateSize() if (wasValid && m_cpuTexture.handle() != oldTexture) m_convexHullDirty = true; + + m_transformedHullDirty = true; } } @@ -808,13 +775,49 @@ void RenderedTarget::updateHullPoints() return; } - m_hullPoints = textureManager()->getTextureConvexHullPoints(m_cpuTexture); - // TODO: Apply graphic effects (#117) + textureManager()->getTextureConvexHullPoints(m_cpuTexture, m_skin->getTexture(1).size(), m_graphicEffectMask, m_graphicEffects, m_hullPoints); +} + +const std::vector &RenderedTarget::transformedHullPoints() const +{ + // https://github.com/scratchfoundation/scratch-render/blob/9fe90e8f4c2da35d4684359f84b69c264d884133/src/Drawable.js#L594-L616 + if (!m_transformedHullDirty) + return m_transformedHullPoints; + + m_transformedHullPoints.clear(); + + if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.isValid()) + return m_transformedHullPoints; + + const double textureScale = m_skin->getTextureScale(m_cpuTexture); + const double bitmapRes = m_costume->bitmapResolution(); + const double width = m_cpuTexture.width() * m_size / textureScale; + const double height = m_cpuTexture.height() * m_size / textureScale; + const double originX = m_costume->rotationCenterX() * m_size / bitmapRes - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / bitmapRes + height / 2; + const double rot = -rotation() * pi / 180; + const double sinRot = std::sin(rot); + const double cosRot = std::cos(rot); + + const std::vector &points = hullPoints(); + m_transformedHullPoints.reserve(points.size()); + + for (const QPoint &point : points) { + double x = point.x() * m_size / textureScale / bitmapRes - width / 2; + double y = height / 2 - point.y() * m_size / textureScale / bitmapRes; + const QPointF transformed = transformPoint(x, y, originX, originY, sinRot, cosRot); + x = transformed.x() * (m_mirrorHorizontally ? -1 : 1); + y = transformed.y(); + m_transformedHullPoints.push_back(QPointF(x, y)); + } + + m_transformedHullDirty = false; + return m_transformedHullPoints; } bool RenderedTarget::containsLocalPoint(const QPointF &point) const { - return textureManager()->textureContainsPoint(m_cpuTexture, point); + return textureManager()->textureContainsPoint(m_cpuTexture, point, m_graphicEffectMask, m_graphicEffects); } QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const @@ -870,36 +873,71 @@ CpuTextureManager *RenderedTarget::textureManager() const return m_textureManager.get(); } -void RenderedTarget::getVisibleTargets(std::vector &dst) const +bool RenderedTarget::touchingColor(Rgb color, bool hasMask, Rgb mask) const { - dst.clear(); - + // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L775-L841 if (!m_engine) - return; + return false; - const auto &targets = m_engine->targets(); + QRgb rgb = qRgb(qRed(color), qGreen(color), qBlue(color)); // ignore alpha + QRgb mask3b; + double ghostValue = 0; - for (auto target : targets) { - Q_ASSERT(target); + if (hasMask) { + // Ignore ghost effect when checking mask + if ((m_graphicEffectMask & ShaderManager::Effect::Ghost) != 0) { + ghostValue = m_graphicEffects[ShaderManager::Effect::Ghost]; + m_graphicEffects.erase(ShaderManager::Effect::Ghost); + m_graphicEffectMask &= ~ShaderManager::Effect::Ghost; + } - if (target->isStage()) - dst.push_back(target.get()); - else { - Sprite *sprite = static_cast(target.get()); + mask3b = qRgb(qRed(mask), qGreen(mask), qBlue(mask)); // ignore alpha + } - if (sprite->visible()) - dst.push_back(target.get()); + std::vector targets; + m_engine->getVisibleTargets(targets); - const auto &clones = sprite->clones(); + QRectF myRect = touchingBounds(); + std::vector candidates; + QRectF bounds = candidatesBounds(myRect, targets, candidates); + + if (colorMatches(rgb, qRgb(255, 255, 255))) { + // The color we're checking for is the background color which spans the entire stage + bounds = myRect; + + if (bounds.isEmpty()) + return false; + } else if (candidates.empty()) { + // If not checking for the background color, we can return early if there are no candidate drawables + return false; + } + + // Loop through the points of the union + for (int y = bounds.top(); y <= bounds.bottom(); y++) { + for (int x = bounds.left(); x <= bounds.right(); x++) { + if (hasMask ? maskMatches(colorAtScratchPoint(x, y), mask3b) : this->containsScratchPoint(x, y)) { + QRgb pixelColor = sampleColor3b(x, y, candidates); + + if (colorMatches(rgb, pixelColor)) { + // Restore ghost effect value + if (hasMask && ghostValue != 0) { + m_graphicEffects[ShaderManager::Effect::Ghost] = ghostValue; + m_graphicEffectMask |= ShaderManager::Effect::Ghost; + } - for (auto clone : clones) { - if (clone->visible()) - dst.push_back(clone.get()); + return true; + } } } } - std::sort(dst.begin(), dst.end(), [](Target *t1, Target *t2) { return t1->layerOrder() > t2->layerOrder(); }); + // Restore ghost effect value + if (hasMask && ghostValue != 0) { + m_graphicEffects[ShaderManager::Effect::Ghost] = ghostValue; + m_graphicEffectMask |= ShaderManager::Effect::Ghost; + } + + return false; } QRectF RenderedTarget::touchingBounds() const @@ -1027,37 +1065,16 @@ void RenderedTarget::clampRect(Rect &rect, double left, double right, double bot rect.setTop(std::max(rect.top(), bottom)); } -QRgb RenderedTarget::convertColor(const libscratchcpp::Value &color) +bool RenderedTarget::colorMatches(QRgb a, QRgb b) { - // TODO: Remove this after libscratchcpp starts converting colors (it still needs to be converted to RGB here) - std::string stringValue; - - if (color.isString()) - stringValue = color.toString(); - - if (!stringValue.empty() && stringValue[0] == '#') { - bool valid = false; - QColor color; - - if (stringValue.size() <= 7) // #RRGGBB - { - color = QColor::fromString(stringValue); - valid = color.isValid(); - } - - if (!valid) - color = Qt::black; - - return color.rgb(); - - } else - return QColor::fromRgba(static_cast(color.toLong())).rgb(); + // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L77-L81 + return qAlpha(a) > 0 && (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000); } -bool RenderedTarget::colorMatches(QRgb a, QRgb b) +bool RenderedTarget::maskMatches(QRgb a, QRgb b) { - // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L77-L81 - return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000); + // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L59-L65 + return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11111000) == (qBlue(b) & 0b11111000); } QRgb RenderedTarget::sampleColor3b(double x, double y, const std::vector &targets) const diff --git a/src/renderedtarget.h b/src/renderedtarget.h index e752e1c..5a94bb2 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -101,7 +101,8 @@ class RenderedTarget : public IRenderedTarget QRgb colorAtScratchPoint(double x, double y) const override; bool touchingClones(const std::vector &) const override; - bool touchingColor(const libscratchcpp::Value &color) const override; + bool touchingColor(libscratchcpp::Rgb color) const override; + bool touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const override; signals: void engineChanged(); @@ -124,21 +125,22 @@ class RenderedTarget : public IRenderedTarget void handleSceneMouseMove(qreal x, qreal y); bool convexHullPointsNeeded() const; void updateHullPoints(); + const std::vector &transformedHullPoints() const; bool containsLocalPoint(const QPointF &point) const; QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const; QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const; QPointF mapFromScratchToLocal(const QPointF &point) const; CpuTextureManager *textureManager() const; - void getVisibleTargets(std::vector &dst) const; + bool touchingColor(libscratchcpp::Rgb color, bool hasMask, libscratchcpp::Rgb mask) const; QRectF touchingBounds() const; QRectF candidatesBounds(const QRectF &targetRect, const std::vector &candidates, std::vector &dst) const; QRectF candidatesBounds(const QRectF &targetRect, const std::vector &candidates, std::vector &dst) const; static QRectF candidateIntersection(const QRectF &targetRect, IRenderedTarget *target); static QRectF rectIntersection(const QRectF &targetRect, const libscratchcpp::Rect &candidateRect); static void clampRect(libscratchcpp::Rect &rect, double left, double right, double bottom, double top); - static QRgb convertColor(const libscratchcpp::Value &color); static bool colorMatches(QRgb a, QRgb b); + static bool maskMatches(QRgb a, QRgb b); QRgb sampleColor3b(double x, double y, const std::vector &targets) const; libscratchcpp::IEngine *m_engine = nullptr; @@ -156,7 +158,8 @@ class RenderedTarget : public IRenderedTarget Texture m_cpuTexture; // without stage scale mutable std::shared_ptr m_textureManager; // NOTE: Use textureManager()! std::unique_ptr m_glF; - std::unordered_map m_graphicEffects; + mutable std::unordered_map m_graphicEffects; + mutable ShaderManager::Effect m_graphicEffectMask = ShaderManager::Effect::NoEffect; double m_size = 1; double m_x = 0; double m_y = 0; @@ -170,7 +173,9 @@ class RenderedTarget : public IRenderedTarget qreal m_maximumHeight = std::numeric_limits::infinity(); bool m_convexHullDirty = true; std::vector m_hullPoints; - bool m_clicked = false; // left mouse button only! + mutable bool m_transformedHullDirty = true; + mutable std::vector m_transformedHullPoints; // NOTE: Use transformedHullPoints(); + bool m_clicked = false; // left mouse button only! double m_dragX = 0; double m_dragY = 0; double m_dragDeltaX = 0; diff --git a/src/shadermanager.cpp b/src/shadermanager.cpp index a87f9b7..93625ba 100644 --- a/src/shadermanager.cpp +++ b/src/shadermanager.cpp @@ -12,6 +12,8 @@ using namespace scratchcpprender; using ConverterFunc = float (*)(float); +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 + static float wrapClamp(float n, float min, float max) { // TODO: Move this to a separate class @@ -22,22 +24,41 @@ static float wrapClamp(float n, float min, float max) static const QString VERTEX_SHADER_SRC = ":/qt/qml/ScratchCPP/Render/shaders/sprite.vert"; static const QString FRAGMENT_SHADER_SRC = ":/qt/qml/ScratchCPP/Render/shaders/sprite.frag"; +#ifdef Q_OS_MACOS +static const QString SHADER_PREFIX = "#version 410\n"; +#else +static const QString SHADER_PREFIX = "#version 300 es\n"; +#endif + static const char *TEXTURE_UNIT_UNIFORM = "u_skin"; +static const char *SKIN_SIZE_UNIFORM = "u_skinSize"; -static const std::unordered_map - EFFECT_TO_NAME = { { ShaderManager::Effect::Color, "color" }, { ShaderManager::Effect::Brightness, "brightness" }, { ShaderManager::Effect::Ghost, "ghost" } }; +static const std::unordered_map EFFECT_TO_NAME = { + { ShaderManager::Effect::Color, "color" }, { ShaderManager::Effect::Brightness, "brightness" }, { ShaderManager::Effect::Ghost, "ghost" }, { ShaderManager::Effect::Fisheye, "fisheye" }, + { ShaderManager::Effect::Whirl, "whirl" }, { ShaderManager::Effect::Pixelate, "pixelate" }, { ShaderManager::Effect::Mosaic, "mosaic" } +}; -static const std::unordered_map - EFFECT_UNIFORM_NAME = { { ShaderManager::Effect::Color, "u_color" }, { ShaderManager::Effect::Brightness, "u_brightness" }, { ShaderManager::Effect::Ghost, "u_ghost" } }; +static const std::unordered_map EFFECT_UNIFORM_NAME = { + { ShaderManager::Effect::Color, "u_color" }, { ShaderManager::Effect::Brightness, "u_brightness" }, { ShaderManager::Effect::Ghost, "u_ghost" }, { ShaderManager::Effect::Fisheye, "u_fisheye" }, + { ShaderManager::Effect::Whirl, "u_whirl" }, { ShaderManager::Effect::Pixelate, "u_pixelate" }, { ShaderManager::Effect::Mosaic, "u_mosaic" } +}; static const std::unordered_map EFFECT_CONVERTER = { { ShaderManager::Effect::Color, [](float x) { return wrapClamp(x / 200.0f, 0.0f, 1.0f); } }, { ShaderManager::Effect::Brightness, [](float x) { return std::clamp(x, -100.0f, 100.0f) / 100.0f; } }, - { ShaderManager::Effect::Ghost, [](float x) { return 1 - std::clamp(x, 0.0f, 100.0f) / 100.0f; } } + { ShaderManager::Effect::Ghost, [](float x) { return 1 - std::clamp(x, 0.0f, 100.0f) / 100.0f; } }, + { ShaderManager::Effect::Fisheye, [](float x) { return std::max(0.0f, (x + 100.0f) / 100.0f); } }, + { ShaderManager::Effect::Whirl, [](float x) { return x * (float)pi / 180.0f; } }, + { ShaderManager::Effect::Pixelate, [](float x) { return std::abs(x) / 10.0f; } }, + { ShaderManager::Effect::Mosaic, [](float x) { return std::max(1.0f, std::min(std::round((std::abs(x) + 10.0f) / 10.0f), 512.0f)); } } +}; + +static const std::unordered_map EFFECT_SHAPE_CHANGES = { + { ShaderManager::Effect::Color, false }, { ShaderManager::Effect::Brightness, false }, { ShaderManager::Effect::Ghost, false }, { ShaderManager::Effect::Fisheye, true }, + { ShaderManager::Effect::Whirl, true }, { ShaderManager::Effect::Pixelate, true }, { ShaderManager::Effect::Mosaic, true } }; -static const std::unordered_map - EFFECT_SHAPE_CHANGES = { { ShaderManager::Effect::Color, false }, { ShaderManager::Effect::Brightness, false }, { ShaderManager::Effect::Ghost, false } }; +std::unordered_set ShaderManager::m_effects; // populated by effects() Q_GLOBAL_STATIC(ShaderManager, globalInstance) @@ -58,7 +79,7 @@ ShaderManager::ShaderManager(QObject *parent) : QByteArray vertexShaderSource; QFile vertSource(VERTEX_SHADER_SRC); vertSource.open(QFile::ReadOnly); - vertexShaderSource = "#version 330 core\n" + vertSource.readAll(); + vertexShaderSource = SHADER_PREFIX.toUtf8() + vertSource.readAll(); m_vertexShader = new QOpenGLShader(QOpenGLShader::Vertex, this); m_vertexShader->compileSourceCode(vertexShaderSource); @@ -101,12 +122,10 @@ QOpenGLShaderProgram *ShaderManager::getShaderProgram(const std::unordered_mapsecond; } -void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit, const std::unordered_map &effectValues) +void ShaderManager::getUniformValuesForEffects(const std::unordered_map &effectValues, std::unordered_map &dst) { - // Set the texture unit - program->setUniformValue(TEXTURE_UNIT_UNIFORM, textureUnit); + dst.clear(); - // Set the uniform values for the enabled effects and reset the other effects for (const auto &[effect, name] : EFFECT_TO_NAME) { const auto it = effectValues.find(effect); double value; @@ -117,8 +136,40 @@ void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit, value = it->second; auto converter = EFFECT_CONVERTER.at(effect); - program->setUniformValue(EFFECT_UNIFORM_NAME.at(effect), converter(value)); + dst[effect] = converter(value); + } +} + +void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit, const QSize skinSize, const std::unordered_map &effectValues) +{ + // Set the texture unit + program->setUniformValue(TEXTURE_UNIT_UNIFORM, textureUnit); + + // Set skin size + program->setUniformValue(SKIN_SIZE_UNIFORM, QVector2D(skinSize.width(), skinSize.height())); + + // Set uniform values + std::unordered_map values; + getUniformValuesForEffects(effectValues, values); + + for (const auto &[effect, value] : values) + program->setUniformValue(EFFECT_UNIFORM_NAME.at(effect), value); +} + +const std::unordered_set &ShaderManager::effects() +{ + if (m_effects.empty()) { + for (const auto &[effect, name] : EFFECT_TO_NAME) + m_effects.insert(effect); } + + return m_effects; +} + +bool ShaderManager::effectShapeChanges(Effect effect) +{ + Q_ASSERT(EFFECT_SHAPE_CHANGES.find(effect) != EFFECT_SHAPE_CHANGES.cend()); + return EFFECT_SHAPE_CHANGES.at(effect); } void ShaderManager::registerEffects() @@ -139,7 +190,7 @@ QOpenGLShaderProgram *ShaderManager::createShaderProgram(const std::unordered_ma return nullptr; // Version must be defined in the first line - QByteArray fragSource = "#version 330\n"; + QByteArray fragSource = SHADER_PREFIX.toUtf8(); // Add defines for the effects for (const auto &[effect, value] : effectValues) { diff --git a/src/shadermanager.h b/src/shadermanager.h index dde8245..210ee98 100644 --- a/src/shadermanager.h +++ b/src/shadermanager.h @@ -4,6 +4,7 @@ #include #include +#include class QOpenGLShaderProgram; class QOpenGLShader; @@ -16,9 +17,14 @@ class ShaderManager : public QObject public: enum class Effect { + NoEffect = 0, Color = 1 << 0, Brightness = 1 << 1, - Ghost = 1 << 2 + Ghost = 1 << 2, + Fisheye = 1 << 3, + Whirl = 1 << 4, + Pixelate = 1 << 5, + Mosaic = 1 << 6 }; explicit ShaderManager(QObject *parent = nullptr); @@ -26,7 +32,11 @@ class ShaderManager : public QObject static ShaderManager *instance(); QOpenGLShaderProgram *getShaderProgram(const std::unordered_map &effectValues); - void setUniforms(QOpenGLShaderProgram *program, int textureUnit, const std::unordered_map &effectValues); + static void getUniformValuesForEffects(const std::unordered_map &effectValues, std::unordered_map &dst); + void setUniforms(QOpenGLShaderProgram *program, int textureUnit, const QSize skinSize, const std::unordered_map &effectValues); + + static const std::unordered_set &effects(); + static bool effectShapeChanges(Effect effect); private: struct Registrar @@ -39,10 +49,56 @@ class ShaderManager : public QObject QOpenGLShaderProgram *createShaderProgram(const std::unordered_map &effectValues); static Registrar m_registrar; + static std::unordered_set m_effects; QOpenGLShader *m_vertexShader = nullptr; std::unordered_map m_shaderPrograms; QByteArray m_fragmentShaderSource; }; +inline ShaderManager::Effect operator|(ShaderManager::Effect a, ShaderManager::Effect b) +{ + return static_cast(static_cast(a) | static_cast(b)); +} + +inline ShaderManager::Effect operator|=(ShaderManager::Effect &a, ShaderManager::Effect b) +{ + return static_cast((int &)a |= static_cast(b)); +} + +inline ShaderManager::Effect operator&(ShaderManager::Effect a, ShaderManager::Effect b) +{ + return static_cast(static_cast(a) & static_cast(b)); +} + +inline ShaderManager::Effect operator&=(ShaderManager::Effect &a, ShaderManager::Effect b) +{ + return static_cast((int &)a &= static_cast(b)); +} + +inline ShaderManager::Effect operator~(ShaderManager::Effect a) +{ + return static_cast(~static_cast(a)); +} + +inline bool operator==(ShaderManager::Effect a, int b) +{ + return static_cast(a) == b; +} + +inline bool operator==(int a, ShaderManager::Effect b) +{ + return a == static_cast(b); +} + +inline bool operator!=(ShaderManager::Effect a, int b) +{ + return static_cast(a) != b; +} + +inline bool operator!=(int a, ShaderManager::Effect b) +{ + return a != static_cast(b); +} + } // namespace scratchcpprender diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index 9568bf5..6399587 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -1,5 +1,11 @@ // Ported from https://github.com/scratchfoundation/scratch-render/blob/4090e62e8abf427e55c83448da9b0df26120d2fb/src/shaders/sprite.frag +#undef lowp +#undef mediump +#undef highp + +precision mediump float; + #ifdef ENABLE_color uniform float u_color; #endif // ENABLE_color @@ -12,8 +18,25 @@ uniform float u_brightness; uniform float u_ghost; #endif // ENABLE_ghost +#ifdef ENABLE_fisheye +uniform float u_fisheye; +#endif // ENABLE_fisheye + +#ifdef ENABLE_whirl +uniform float u_whirl; +#endif // ENABLE_whirl + +#ifdef ENABLE_pixelate +uniform float u_pixelate; +uniform vec2 u_skinSize; +#endif // ENABLE_pixelate + +#ifdef ENABLE_mosaic +uniform float u_mosaic; +#endif // ENABLE_mosaic + in vec2 v_texCoord; -//out vec4 FragColor; +out vec4 fragColor; uniform sampler2D u_skin; // Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations. @@ -82,11 +105,59 @@ const vec2 kCenter = vec2(0.5, 0.5); void main() { - vec4 texColor = texture2D(u_skin, v_texCoord); + vec2 texcoord0 = v_texCoord; + + #ifdef ENABLE_mosaic + texcoord0 = fract(u_mosaic * texcoord0); + #endif // ENABLE_mosaic + + #ifdef ENABLE_pixelate + { + // TODO: clean up "pixel" edges + vec2 pixelTexelSize = u_skinSize / u_pixelate; + texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / pixelTexelSize; + } + #endif // ENABLE_pixelate + + #ifdef ENABLE_whirl + { + const float kRadius = 0.5; + vec2 offset = texcoord0 - kCenter; + float offsetMagnitude = length(offset); + float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0); + float whirlActual = u_whirl * whirlFactor * whirlFactor; + float sinWhirl = sin(whirlActual); + float cosWhirl = cos(whirlActual); + mat2 rotationMatrix = mat2( + cosWhirl, -sinWhirl, + sinWhirl, cosWhirl + ); + + texcoord0 = rotationMatrix * offset + kCenter; + } + #endif // ENABLE_whirl + + #ifdef ENABLE_fisheye + { + vec2 vec = (texcoord0 - kCenter) / kCenter; + float vecLength = length(vec); + float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength); + vec2 unit = vec / vecLength; + + texcoord0 = kCenter + r * unit * kCenter; + } + #endif // ENABLE_fisheye + + fragColor = texture(u_skin, texcoord0); + + #if defined(ENABLE_color) || defined(ENABLE_brightness) + // Divide premultiplied alpha values for proper color processing + // Add epsilon to avoid dividing by 0 for fully transparent pixels + fragColor.rgb = clamp(fragColor.rgb / (fragColor.a + epsilon), 0.0, 1.0); #ifdef ENABLE_color { - vec3 hsv = convertRGB2HSV(texColor.rgb); + vec3 hsv = convertRGB2HSV(fragColor.rgb); // Force grayscale values to be slightly saturated const float minLightness = 0.11 / 2.0; @@ -97,22 +168,20 @@ void main() hsv.x = mod(hsv.x + u_color, 1.0); if (hsv.x < 0.0) hsv.x += 1.0; - texColor.rgb = convertHSV2RGB(hsv); + fragColor.rgb = convertHSV2RGB(hsv); } #endif // ENABLE_color #ifdef ENABLE_brightness - texColor.rgb = clamp(texColor.rgb + vec3(u_brightness), vec3(0), vec3(1)); + fragColor.rgb = clamp(fragColor.rgb + vec3(u_brightness), vec3(0), vec3(1)); #endif // ENABLE_brightness + // Re-multiply color values + fragColor.rgb *= fragColor.a + epsilon; + + #endif // defined(ENABLE_color) || defined(ENABLE_brightness) + #ifdef ENABLE_ghost - texColor *= u_ghost; + fragColor *= u_ghost; #endif // ENABLE_ghost - - // Set RGB components to zero if the color is fully transparent - // This is a workaround for rendering issues when alpha is zero - if(texColor.a == 0.0) - gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); - else - gl_FragColor = texColor; } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index f7d09b0..b52cc8f 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -1,9 +1,11 @@ -layout (location = 0) in vec2 a_position; -layout (location = 1) in vec2 a_texCoord; +uniform mat4 u_projectionMatrix; +uniform mat4 u_modelMatrix; +in vec2 a_position; +in vec2 a_texCoord; out vec2 v_texCoord; void main() { - gl_Position = vec4(a_position, 0.0, 1.0); + gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1); v_texCoord = a_texCoord; } diff --git a/src/skin.cpp b/src/skin.cpp index 5d25b48..6ac4d6b 100644 --- a/src/skin.cpp +++ b/src/skin.cpp @@ -12,11 +12,13 @@ Skin::Skin() QOpenGLContext *context = QOpenGLContext::currentContext(); Q_ASSERT(context); - if (context) { - QObject::connect(context, &QOpenGLContext::aboutToBeDestroyed, &m_signalHandler, [this]() { + if (!m_connectedCtx || (context && context != m_connectedCtx)) { + QObject::connect(context, &QOpenGLContext::aboutToBeDestroyed, []() { // Destroy textures m_textures.clear(); }); + + m_connectedCtx = context; } } @@ -56,8 +58,12 @@ Texture Skin::createAndPaintTexture(int width, int height) // Create final texture from the image auto texture = std::make_shared(image); m_textures.push_back(texture); - texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear); - texture->setMagnificationFilter(QOpenGLTexture::Linear); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + texture->bind(); + glF.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glF.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + texture->release(); return Texture(texture->textureId(), width, height); } diff --git a/src/skin.h b/src/skin.h index 855ef65..4113024 100644 --- a/src/skin.h +++ b/src/skin.h @@ -26,8 +26,8 @@ class Skin virtual void paint(QPainter *painter) = 0; private: - std::vector> m_textures; - QObject m_signalHandler; // for disconnecting signals after destroyed + static inline std::vector> m_textures; + static inline QOpenGLContext *m_connectedCtx = nullptr; }; } // namespace scratchcpprender diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 70e440e..569ea96 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -3,23 +3,24 @@ #include #include #include +#include #include "spritemodel.h" -#include "renderedtarget.h" -#include "ipenlayer.h" -#include "graphicseffect.h" +#include "penlayer.h" -namespace scratchcpprender -{ +using namespace scratchcpprender; SpriteModel::SpriteModel(QObject *parent) : - QObject(parent) + TargetModel(parent) { } void SpriteModel::init(libscratchcpp::Sprite *sprite) { m_sprite = sprite; + + if (m_sprite) + setupTextBubble(m_sprite->bubble()); } void SpriteModel::deinitClone() @@ -34,206 +35,119 @@ void SpriteModel::onCloned(libscratchcpp::Sprite *clone) SpriteModel *cloneModel = new SpriteModel(m_cloneRoot); cloneModel->m_cloneRoot = m_cloneRoot; - cloneModel->m_penLayer = m_penLayer; - cloneModel->m_penState = m_penState; + cloneModel->setPenLayer(penLayer()); + cloneModel->penState() = penState(); clone->setInterface(cloneModel); emit cloned(cloneModel); } void SpriteModel::onCostumeChanged(libscratchcpp::Costume *costume) { - if (m_renderedTarget) - m_renderedTarget->updateCostume(costume); + updateCostume(costume); } void SpriteModel::onVisibleChanged(bool visible) { - if (m_renderedTarget) - m_renderedTarget->updateVisibility(visible); + updateVisibility(visible); } void SpriteModel::onXChanged(double x) { - if (m_renderedTarget) - m_renderedTarget->updateX(x); + updateX(x); } void SpriteModel::onYChanged(double y) { - if (m_renderedTarget) - m_renderedTarget->updateY(y); + updateY(y); } void SpriteModel::onMoved(double oldX, double oldY, double newX, double newY) { - if (m_penState.penDown && m_penLayer) { - m_penLayer->drawLine(m_penState.penAttributes, oldX, oldY, newX, newY); - libscratchcpp::IEngine *engine = m_sprite->engine(); - - if (engine) - engine->requestRedraw(); - } + TargetModel::onMoved(oldX, oldY, newX, newY); } void SpriteModel::onSizeChanged(double size) { - if (m_renderedTarget) - m_renderedTarget->updateSize(size); + updateSize(size); } void SpriteModel::onDirectionChanged(double direction) { - if (m_renderedTarget) - m_renderedTarget->updateDirection(direction); + updateDirection(direction); } void SpriteModel::onRotationStyleChanged(libscratchcpp::Sprite::RotationStyle rotationStyle) { - if (m_renderedTarget) - m_renderedTarget->updateRotationStyle(rotationStyle); + updateRotationStyle(rotationStyle); } void SpriteModel::onLayerOrderChanged(int layerOrder) { - if (m_renderedTarget) - m_renderedTarget->updateLayerOrder(layerOrder); + updateLayerOrder(layerOrder); } void SpriteModel::onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) { - GraphicsEffect *graphicsEffect = dynamic_cast(effect); - - if (graphicsEffect && m_renderedTarget) - m_renderedTarget->setGraphicEffect(graphicsEffect->effect(), value); + setGraphicEffect(effect, value); } void SpriteModel::onGraphicsEffectsCleared() { - if (m_renderedTarget) - m_renderedTarget->clearGraphicEffects(); -} - -void SpriteModel::onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) -{ - if (type == libscratchcpp::Target::BubbleType::Say) { - if (m_bubbleType == TextBubbleShape::Type::Say) - return; - - m_bubbleType = TextBubbleShape::Type::Say; - } else { - if (m_bubbleType == TextBubbleShape::Type::Think) - return; - - m_bubbleType = TextBubbleShape::Type::Think; - } - - emit bubbleTypeChanged(); -} - -void SpriteModel::onBubbleTextChanged(const std::string &text) -{ - QString newText = QString::fromStdString(text); - - if (m_bubbleText != newText) { - m_bubbleText = newText; - emit bubbleTextChanged(); - } + clearGraphicEffects(); } int SpriteModel::costumeWidth() const { - return m_renderedTarget->costumeWidth(); + return TargetModel::costumeWidth(); } int SpriteModel::costumeHeight() const { - return m_renderedTarget->costumeHeight(); + return TargetModel::costumeHeight(); } libscratchcpp::Rect SpriteModel::boundingRect() const { - return m_renderedTarget->getBounds(); + libscratchcpp::Rect ret; + getBoundingRect(ret); + return ret; } libscratchcpp::Rect SpriteModel::fastBoundingRect() const { - return m_renderedTarget->getFastBounds(); + libscratchcpp::Rect ret; + getFastBoundingRect(ret); + return ret; } bool SpriteModel::touchingClones(const std::vector &clones) const { - return m_renderedTarget->touchingClones(clones); + return TargetModel::touchingClones(clones); } bool SpriteModel::touchingPoint(double x, double y) const { - return m_renderedTarget->containsScratchPoint(x, y); -} - -bool SpriteModel::touchingColor(const libscratchcpp::Value &color) const -{ - return m_renderedTarget->touchingColor(color); -} - -libscratchcpp::Sprite *SpriteModel::sprite() const -{ - return m_sprite; -} - -IRenderedTarget *SpriteModel::renderedTarget() const -{ - return m_renderedTarget; -} - -void SpriteModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) -{ - if (m_renderedTarget == newRenderedTarget) - return; - - m_renderedTarget = newRenderedTarget; - emit renderedTargetChanged(); -} - -IPenLayer *SpriteModel::penLayer() const -{ - return m_penLayer; -} - -void SpriteModel::setPenLayer(IPenLayer *newPenLayer) -{ - if (m_penLayer == newPenLayer) - return; - - m_penLayer = newPenLayer; - emit penLayerChanged(); + return TargetModel::touchingPoint(x, y); } -PenState &SpriteModel::penState() +bool SpriteModel::touchingColor(libscratchcpp::Rgb color) const { - return m_penState; + return TargetModel::touchingColor(color); } -PenAttributes &SpriteModel::penAttributes() +bool SpriteModel::touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const { - return m_penState.penAttributes; + return TargetModel::touchingColor(color, mask); } -bool SpriteModel::penDown() const +libscratchcpp::Sprite *SpriteModel::sprite() const { - return m_penState.penDown; + return m_sprite; } -void SpriteModel::setPenDown(bool newPenDown) +int SpriteModel::bubbleLayer() const { - m_penState.penDown = newPenDown; - - if (m_penState.penDown && m_penLayer && m_sprite) { - m_penLayer->drawPoint(m_penState.penAttributes, m_sprite->x(), m_sprite->y()); - libscratchcpp::IEngine *engine = m_sprite->engine(); - - if (engine) - engine->requestRedraw(); - } + return m_sprite ? m_sprite->bubble()->layerOrder() : 0; } SpriteModel *SpriteModel::cloneRoot() const @@ -244,14 +158,26 @@ SpriteModel *SpriteModel::cloneRoot() const return m_cloneRoot; } -const TextBubbleShape::Type &SpriteModel::bubbleType() const +void SpriteModel::loadCostume() { - return m_bubbleType; + if (m_sprite) + updateCostume(m_sprite->currentCostume().get()); } -const QString &SpriteModel::bubbleText() const +void SpriteModel::drawPenPoint(IPenLayer *penLayer, const PenAttributes &penAttributes) { - return m_bubbleText; + penLayer->drawPoint(penAttributes, m_sprite->x(), m_sprite->y()); + libscratchcpp::IEngine *engine = m_sprite->engine(); + + if (engine) + engine->requestRedraw(); } -} // namespace scratchcpprender +void SpriteModel::drawPenLine(IPenLayer *penLayer, const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) +{ + penLayer->drawLine(penAttributes, x0, y0, x1, y1); + libscratchcpp::IEngine *engine = m_sprite->engine(); + + if (engine) + engine->requestRedraw(); +} diff --git a/src/spritemodel.h b/src/spritemodel.h index 52e2076..6428396 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -2,32 +2,19 @@ #pragma once -#include -#include #include -#include "penstate.h" -#include "textbubbleshape.h" - -Q_MOC_INCLUDE("renderedtarget.h"); -Q_MOC_INCLUDE("ipenlayer.h"); +#include "targetmodel.h" namespace scratchcpprender { -class IRenderedTarget; -class IPenLayer; - class SpriteModel - : public QObject + : public TargetModel , public libscratchcpp::ISpriteHandler { Q_OBJECT QML_ELEMENT - Q_PROPERTY(IRenderedTarget *renderedTarget READ renderedTarget WRITE setRenderedTarget NOTIFY renderedTargetChanged) - Q_PROPERTY(IPenLayer *penLayer READ penLayer WRITE setPenLayer NOTIFY penLayerChanged) - Q_PROPERTY(TextBubbleShape::Type bubbleType READ bubbleType NOTIFY bubbleTypeChanged) - Q_PROPERTY(QString bubbleText READ bubbleText NOTIFY bubbleTextChanged) public: SpriteModel(QObject *parent = nullptr); @@ -51,9 +38,6 @@ class SpriteModel void onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) override; void onGraphicsEffectsCleared() override; - void onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) override; - void onBubbleTextChanged(const std::string &text) override; - int costumeWidth() const override; int costumeHeight() const override; @@ -62,44 +46,28 @@ class SpriteModel bool touchingClones(const std::vector &clones) const override; bool touchingPoint(double x, double y) const override; - bool touchingColor(const libscratchcpp::Value &color) const override; + bool touchingColor(libscratchcpp::Rgb color) const override; + bool touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const override; libscratchcpp::Sprite *sprite() const; - IRenderedTarget *renderedTarget() const; - void setRenderedTarget(IRenderedTarget *newRenderedTarget); - - IPenLayer *penLayer() const; - void setPenLayer(IPenLayer *newPenLayer); - - PenState &penState(); - PenAttributes &penAttributes(); - - bool penDown() const; - void setPenDown(bool newPenDown); + int bubbleLayer() const override; SpriteModel *cloneRoot() const; - const TextBubbleShape::Type &bubbleType() const; - - const QString &bubbleText() const; + Q_INVOKABLE void loadCostume() override; signals: - void renderedTargetChanged(); - void penLayerChanged(); - void bubbleTypeChanged(); - void bubbleTextChanged(); void cloned(SpriteModel *cloneModel); void cloneDeleted(SpriteModel *clone); + protected: + void drawPenPoint(IPenLayer *penLayer, const PenAttributes &penAttributes) override; + void drawPenLine(IPenLayer *penLayer, const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) override; + private: libscratchcpp::Sprite *m_sprite = nullptr; - IRenderedTarget *m_renderedTarget = nullptr; - IPenLayer *m_penLayer = nullptr; - PenState m_penState; SpriteModel *m_cloneRoot = nullptr; - TextBubbleShape::Type m_bubbleType = TextBubbleShape::Type::Say; - QString m_bubbleText; }; } // namespace scratchcpprender diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 3290424..4aa3f5b 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -1,27 +1,30 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include +#include +#include #include "stagemodel.h" -#include "renderedtarget.h" -#include "graphicseffect.h" +#include "penlayer.h" using namespace scratchcpprender; StageModel::StageModel(QObject *parent) : - QObject(parent) + TargetModel(parent) { } void StageModel::init(libscratchcpp::Stage *stage) { m_stage = stage; + + if (m_stage) + setupTextBubble(m_stage->bubble()); } void StageModel::onCostumeChanged(libscratchcpp::Costume *costume) { - if (m_renderedTarget) - m_renderedTarget->updateCostume(costume); + updateCostume(costume); } void StageModel::onTempoChanged(int tempo) @@ -38,84 +41,56 @@ void StageModel::onVideoTransparencyChanged(int videoTransparency) void StageModel::onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) { - GraphicsEffect *graphicsEffect = dynamic_cast(effect); - - if (graphicsEffect && m_renderedTarget) - m_renderedTarget->setGraphicEffect(graphicsEffect->effect(), value); + setGraphicEffect(effect, value); } void StageModel::onGraphicsEffectsCleared() { - if (m_renderedTarget) - m_renderedTarget->clearGraphicEffects(); -} - -void StageModel::onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) -{ - if (type == libscratchcpp::Target::BubbleType::Say) { - if (m_bubbleType == TextBubbleShape::Type::Say) - return; - - m_bubbleType = TextBubbleShape::Type::Say; - } else { - if (m_bubbleType == TextBubbleShape::Type::Think) - return; - - m_bubbleType = TextBubbleShape::Type::Think; - } - - emit bubbleTypeChanged(); -} - -void StageModel::onBubbleTextChanged(const std::string &text) -{ - QString newText = QString::fromStdString(text); - - if (m_bubbleText != newText) { - m_bubbleText = newText; - emit bubbleTextChanged(); - } + clearGraphicEffects(); } int StageModel::costumeWidth() const { - return m_renderedTarget->costumeWidth(); + return TargetModel::costumeWidth(); } int StageModel::costumeHeight() const { - return m_renderedTarget->costumeHeight(); + return TargetModel::costumeHeight(); } libscratchcpp::Rect StageModel::boundingRect() const { - return libscratchcpp::Rect(); + libscratchcpp::Rect ret; + getBoundingRect(ret); + return ret; } libscratchcpp::Rect StageModel::fastBoundingRect() const { - return libscratchcpp::Rect(); + libscratchcpp::Rect ret; + getFastBoundingRect(ret); + return ret; } bool StageModel::touchingClones(const std::vector &clones) const { - return m_renderedTarget->touchingClones(clones); + return TargetModel::touchingClones(clones); } bool StageModel::touchingPoint(double x, double y) const { - return m_renderedTarget->containsScratchPoint(x, y); + return TargetModel::touchingPoint(x, y); } -bool StageModel::touchingColor(const libscratchcpp::Value &color) const +bool StageModel::touchingColor(libscratchcpp::Rgb color) const { - return m_renderedTarget->touchingColor(color); + return TargetModel::touchingColor(color); } -void StageModel::loadCostume() +bool StageModel::touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const { - if (m_renderedTarget && m_stage) - m_renderedTarget->updateCostume(m_stage->currentCostume().get()); + return TargetModel::touchingColor(color, mask); } libscratchcpp::Stage *StageModel::stage() const @@ -123,27 +98,22 @@ libscratchcpp::Stage *StageModel::stage() const return m_stage; } -IRenderedTarget *StageModel::renderedTarget() const +int StageModel::bubbleLayer() const { - return m_renderedTarget; + return m_stage ? m_stage->bubble()->layerOrder() : 0; } -void StageModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) +void StageModel::loadCostume() { - if (m_renderedTarget == newRenderedTarget) - return; - - m_renderedTarget = newRenderedTarget; - - emit renderedTargetChanged(); + if (m_stage) + updateCostume(m_stage->currentCostume().get()); } -const TextBubbleShape::Type &StageModel::bubbleType() const +void StageModel::drawPenPoint(IPenLayer *penLayer, const PenAttributes &penAttributes) { - return m_bubbleType; -} + penLayer->drawLine(penAttributes, 0, 0, 0, 0); + libscratchcpp::IEngine *engine = m_stage->engine(); -const QString &StageModel::bubbleText() const -{ - return m_bubbleText; + if (engine) + engine->requestRedraw(); } diff --git a/src/stagemodel.h b/src/stagemodel.h index b5423e8..e7512de 100644 --- a/src/stagemodel.h +++ b/src/stagemodel.h @@ -2,26 +2,18 @@ #pragma once -#include #include -#include "textbubbleshape.h" - -Q_MOC_INCLUDE("renderedtarget.h"); +#include "targetmodel.h" namespace scratchcpprender { -class IRenderedTarget; - class StageModel - : public QObject + : public TargetModel , public libscratchcpp::IStageHandler { Q_OBJECT - Q_PROPERTY(IRenderedTarget *renderedTarget READ renderedTarget WRITE setRenderedTarget NOTIFY renderedTargetChanged) - Q_PROPERTY(TextBubbleShape::Type bubbleType READ bubbleType NOTIFY bubbleTypeChanged) - Q_PROPERTY(QString bubbleText READ bubbleText NOTIFY bubbleTextChanged) public: explicit StageModel(QObject *parent = nullptr); @@ -37,9 +29,6 @@ class StageModel void onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) override; void onGraphicsEffectsCleared() override; - void onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) override; - void onBubbleTextChanged(const std::string &text) override; - int costumeWidth() const override; int costumeHeight() const override; @@ -48,29 +37,20 @@ class StageModel bool touchingClones(const std::vector &clones) const override; bool touchingPoint(double x, double y) const override; - bool touchingColor(const libscratchcpp::Value &color) const override; - - Q_INVOKABLE void loadCostume(); + bool touchingColor(libscratchcpp::Rgb color) const override; + bool touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const override; libscratchcpp::Stage *stage() const; - IRenderedTarget *renderedTarget() const; - void setRenderedTarget(IRenderedTarget *newRenderedTarget); - - const TextBubbleShape::Type &bubbleType() const; + int bubbleLayer() const override; - const QString &bubbleText() const; + Q_INVOKABLE void loadCostume() override; - signals: - void renderedTargetChanged(); - void bubbleTypeChanged(); - void bubbleTextChanged(); + protected: + void drawPenPoint(IPenLayer *penLayer, const PenAttributes &penAttributes) override; private: libscratchcpp::Stage *m_stage = nullptr; - IRenderedTarget *m_renderedTarget = nullptr; - TextBubbleShape::Type m_bubbleType = TextBubbleShape::Type::Say; - QString m_bubbleText; }; } // namespace scratchcpprender diff --git a/src/svgskin.cpp b/src/svgskin.cpp index d2c35a6..55e131e 100644 --- a/src/svgskin.cpp +++ b/src/svgskin.cpp @@ -29,12 +29,6 @@ SVGSkin::SVGSkin(libscratchcpp::Costume *costume) : m_maxIndex = std::min(i1, i2); } -SVGSkin::~SVGSkin() -{ - for (const auto &[index, texture] : m_textures) - m_textureObjects[texture].release(); -} - Texture SVGSkin::getTexture(double scale) const { // https://github.com/scratchfoundation/scratch-render/blob/423bb700c36b8c1c0baae1e2413878a4f778849a/src/SVGSkin.js#L158-L176 diff --git a/src/svgskin.h b/src/svgskin.h index f722e63..2662857 100644 --- a/src/svgskin.h +++ b/src/svgskin.h @@ -21,7 +21,6 @@ class SVGSkin : public Skin { public: SVGSkin(libscratchcpp::Costume *costume); - ~SVGSkin(); Texture getTexture(double scale) const override; double getTextureScale(const Texture &texture) const override; diff --git a/src/targetmodel.cpp b/src/targetmodel.cpp new file mode 100644 index 0000000..82e28db --- /dev/null +++ b/src/targetmodel.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "targetmodel.h" +#include "renderedtarget.h" +#include "penlayer.h" +#include "graphicseffect.h" + +using namespace scratchcpprender; + +TargetModel::TargetModel(QObject *parent) : + QObject(parent) +{ +} + +IRenderedTarget *TargetModel::renderedTarget() const +{ + return m_renderedTarget; +} + +void TargetModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) +{ + if (m_renderedTarget == newRenderedTarget) + return; + + m_renderedTarget = newRenderedTarget; + + emit renderedTargetChanged(); +} + +IPenLayer *TargetModel::penLayer() const +{ + return m_penLayer; +} + +void TargetModel::setPenLayer(IPenLayer *newPenLayer) +{ + if (m_penLayer == newPenLayer) + return; + + m_penLayer = newPenLayer; + emit penLayerChanged(); +} + +PenState &TargetModel::penState() +{ + return m_penState; +} + +PenAttributes &TargetModel::penAttributes() +{ + return m_penState.penAttributes; +} + +bool TargetModel::penDown() const +{ + return m_penState.penDown; +} + +void TargetModel::setPenDown(bool newPenDown) +{ + m_penState.penDown = newPenDown; + + if (m_penState.penDown && m_penLayer) + drawPenPoint(m_penLayer, m_penState.penAttributes); +} + +const TextBubbleShape::Type &TargetModel::bubbleType() const +{ + return m_bubbleType; +} + +const QString &TargetModel::bubbleText() const +{ + return m_bubbleText; +} + +void TargetModel::setupTextBubble(libscratchcpp::TextBubble *bubble) +{ + bubble->typeChanged().connect([this](libscratchcpp::TextBubble::Type type) { + if (type == libscratchcpp::TextBubble::Type::Say) { + if (m_bubbleType == TextBubbleShape::Type::Say) + return; + + m_bubbleType = TextBubbleShape::Type::Say; + } else { + if (m_bubbleType == TextBubbleShape::Type::Think) + return; + + m_bubbleType = TextBubbleShape::Type::Think; + } + + emit bubbleTypeChanged(); + }); + + bubble->textChanged().connect([this](const std::string &text) { + QString newText = QString::fromStdString(text); + + if (m_bubbleText != newText) { + m_bubbleText = newText; + emit bubbleTextChanged(); + } + }); + + bubble->layerOrderChanged().connect([this](int) { emit bubbleLayerChanged(); }); +} + +void TargetModel::updateVisibility(bool visible) +{ + if (m_renderedTarget) + m_renderedTarget->updateVisibility(visible); +} + +void TargetModel::updateX(double x) +{ + if (m_renderedTarget) + m_renderedTarget->updateX(x); +} + +void TargetModel::updateY(double y) +{ + if (m_renderedTarget) + m_renderedTarget->updateY(y); +} + +void TargetModel::updateSize(double size) +{ + if (m_renderedTarget) + m_renderedTarget->updateSize(size); +} + +void TargetModel::updateDirection(double direction) +{ + if (m_renderedTarget) + m_renderedTarget->updateDirection(direction); +} + +void TargetModel::updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) +{ + if (m_renderedTarget) + m_renderedTarget->updateRotationStyle(style); +} + +void TargetModel::updateLayerOrder(int layerOrder) +{ + if (m_renderedTarget) + m_renderedTarget->updateLayerOrder(layerOrder); +} + +void TargetModel::updateCostume(libscratchcpp::Costume *costume) +{ + if (m_renderedTarget) + m_renderedTarget->updateCostume(costume); +} + +void TargetModel::onMoved(double oldX, double oldY, double newX, double newY) +{ + if (m_penState.penDown && m_penLayer) + drawPenLine(m_penLayer, m_penState.penAttributes, oldX, oldY, newX, newY); +} + +void TargetModel::setGraphicEffect(libscratchcpp::IGraphicsEffect *effect, double value) +{ + GraphicsEffect *graphicsEffect = dynamic_cast(effect); + + if (graphicsEffect && m_renderedTarget) + m_renderedTarget->setGraphicEffect(graphicsEffect->effect(), value); +} + +void TargetModel::clearGraphicEffects() +{ + if (m_renderedTarget) + m_renderedTarget->clearGraphicEffects(); +} + +int TargetModel::costumeWidth() const +{ + return m_renderedTarget->costumeWidth(); +} + +int TargetModel::costumeHeight() const +{ + return m_renderedTarget->costumeHeight(); +} + +void TargetModel::getBoundingRect(libscratchcpp::Rect &dst) const +{ + dst = m_renderedTarget->getBounds(); +} + +void TargetModel::getFastBoundingRect(libscratchcpp::Rect &dst) const +{ + dst = m_renderedTarget->getFastBounds(); +} + +bool TargetModel::touchingClones(const std::vector &clones) const +{ + return m_renderedTarget->touchingClones(clones); +} + +bool TargetModel::touchingPoint(double x, double y) const +{ + return m_renderedTarget->containsScratchPoint(x, y); +} + +bool TargetModel::touchingColor(libscratchcpp::Rgb color) const +{ + return m_renderedTarget->touchingColor(color); +} + +bool TargetModel::touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const +{ + return m_renderedTarget->touchingColor(color, mask); +} diff --git a/src/targetmodel.h b/src/targetmodel.h new file mode 100644 index 0000000..2be3e1f --- /dev/null +++ b/src/targetmodel.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include + +#include "penstate.h" +#include "textbubbleshape.h" + +Q_MOC_INCLUDE("renderedtarget.h"); +Q_MOC_INCLUDE("ipenlayer.h"); + +namespace scratchcpprender +{ + +class IRenderedTarget; +class IPenLayer; + +class TargetModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(IRenderedTarget *renderedTarget READ renderedTarget WRITE setRenderedTarget NOTIFY renderedTargetChanged) + Q_PROPERTY(IPenLayer *penLayer READ penLayer WRITE setPenLayer NOTIFY penLayerChanged) + Q_PROPERTY(TextBubbleShape::Type bubbleType READ bubbleType NOTIFY bubbleTypeChanged) + Q_PROPERTY(QString bubbleText READ bubbleText NOTIFY bubbleTextChanged) + Q_PROPERTY(int bubbleLayer READ bubbleLayer NOTIFY bubbleLayerChanged) + + public: + explicit TargetModel(QObject *parent = nullptr); + + IRenderedTarget *renderedTarget() const; + void setRenderedTarget(IRenderedTarget *newRenderedTarget); + + IPenLayer *penLayer() const; + void setPenLayer(IPenLayer *newPenLayer); + + PenState &penState(); + PenAttributes &penAttributes(); + + bool penDown() const; + void setPenDown(bool newPenDown); + + const TextBubbleShape::Type &bubbleType() const; + + const QString &bubbleText() const; + + virtual int bubbleLayer() const { return 0; } + + Q_INVOKABLE virtual void loadCostume() { } + + signals: + void renderedTargetChanged(); + void penLayerChanged(); + void bubbleTypeChanged(); + void bubbleTextChanged(); + void bubbleLayerChanged(); + + protected: + void setupTextBubble(libscratchcpp::TextBubble *bubble); + + void updateVisibility(bool visible); + void updateX(double x); + void updateY(double y); + void updateSize(double size); + void updateDirection(double direction); + void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style); + void updateLayerOrder(int layerOrder); + void updateCostume(libscratchcpp::Costume *costume); + + void onMoved(double oldX, double oldY, double newX, double newY); + + void setGraphicEffect(libscratchcpp::IGraphicsEffect *effect, double value); + void clearGraphicEffects(); + + int costumeWidth() const; + int costumeHeight() const; + + void getBoundingRect(libscratchcpp::Rect &dst) const; + void getFastBoundingRect(libscratchcpp::Rect &dst) const; + + bool touchingClones(const std::vector &clones) const; + bool touchingPoint(double x, double y) const; + bool touchingColor(libscratchcpp::Rgb color) const; + bool touchingColor(libscratchcpp::Rgb color, libscratchcpp::Rgb mask) const; + + virtual void drawPenPoint(IPenLayer *penLayer, const PenAttributes &penAttributes) { } // stage and sprites can draw points + virtual void drawPenLine(IPenLayer *penLayer, const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) { } // only sprites can draw lines + + private: + IRenderedTarget *m_renderedTarget = nullptr; + IPenLayer *m_penLayer = nullptr; + PenState m_penState; + TextBubbleShape::Type m_bubbleType = TextBubbleShape::Type::Say; + QString m_bubbleText; +}; + +} // namespace scratchcpprender diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index bd1cc40..7eb2dd7 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -67,9 +67,7 @@ void TargetPainter::paint(QNanoPainter *painter) Q_ASSERT(shaderProgram->isLinked()); // Set up vertex data and buffers for a quad - float vertices[] = { - -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, - }; + float vertices[] = { -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f }; GLuint VBO, VAO; glF.glGenVertexArrays(1, &VAO); @@ -94,8 +92,10 @@ void TargetPainter::paint(QNanoPainter *painter) glF.glBindVertexArray(VAO); glF.glActiveTexture(GL_TEXTURE0); glF.glBindTexture(GL_TEXTURE_2D, texture.handle()); - shaderManager->setUniforms(shaderProgram, 0, effects); // set texture and effect uniforms - glF.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + shaderManager->setUniforms(shaderProgram, 0, QSize(m_target->costumeWidth(), m_target->costumeHeight()), effects); // set texture and effect uniforms + shaderProgram->setUniformValue("u_projectionMatrix", QMatrix4x4()); + shaderProgram->setUniformValue("u_modelMatrix", QMatrix4x4()); + glF.glDrawArrays(GL_TRIANGLES, 0, 6); // Process the resulting texture // NOTE: This must happen now, not later, because the alpha channel can be used here diff --git a/src/valuemonitormodel.cpp b/src/valuemonitormodel.cpp index 1e55a5d..0af3911 100644 --- a/src/valuemonitormodel.cpp +++ b/src/valuemonitormodel.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -#include #include +#include #include "valuemonitormodel.h" @@ -16,17 +16,15 @@ ValueMonitorModel::ValueMonitorModel(QObject *parent) : { } -ValueMonitorModel::ValueMonitorModel(IBlockSection *section, QObject *parent) : - MonitorModel(section, parent) +ValueMonitorModel::ValueMonitorModel(IExtension *extension, QObject *parent) : + MonitorModel(extension, parent) { } -void ValueMonitorModel::onValueChanged(const VirtualMachine *vm) +void ValueMonitorModel::onValueChanged(const libscratchcpp::Value &value) { - if (vm->registerCount() == 1) { - m_value = QString::fromStdString(vm->getInput(0, 1)->toString()); - emit valueChanged(); - } + m_value = QString::fromStdString(value.toString()); + emit valueChanged(); } MonitorModel::Type ValueMonitorModel::type() const diff --git a/src/valuemonitormodel.h b/src/valuemonitormodel.h index 8f8b3ff..79401b4 100644 --- a/src/valuemonitormodel.h +++ b/src/valuemonitormodel.h @@ -28,9 +28,9 @@ class ValueMonitorModel : public MonitorModel Q_ENUM(Mode) ValueMonitorModel(QObject *parent = nullptr); - ValueMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr); + ValueMonitorModel(libscratchcpp::IExtension *extension, QObject *parent = nullptr); - void onValueChanged(const libscratchcpp::VirtualMachine *vm) override; + void onValueChanged(const libscratchcpp::Value &value) override; Type type() const override; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dcd6bef..f697644 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,8 +36,8 @@ add_subdirectory(penattributes) add_subdirectory(penstate) add_subdirectory(penlayer) add_subdirectory(penlayerpainter) -add_subdirectory(blocks) add_subdirectory(graphicseffect) add_subdirectory(shadermanager) add_subdirectory(textbubbleshape) add_subdirectory(textbubblepainter) +add_subdirectory(effecttransform) diff --git a/test/blocks/CMakeLists.txt b/test/blocks/CMakeLists.txt deleted file mode 100644 index 6d6151d..0000000 --- a/test/blocks/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -# pen_blocks_test -add_executable( - pen_blocks_test - pen_blocks_test.cpp -) - -target_link_libraries( - pen_blocks_test - GTest::gtest_main - GTest::gmock_main - scratchcpp-render - scratchcpprender_mocks - ${QT_LIBS} -) - -add_test(pen_blocks_test) -gtest_discover_tests(pen_blocks_test) - -# penextension_test -add_executable( - penextension_test - penextension_test.cpp -) - -target_link_libraries( - penextension_test - GTest::gtest_main - GTest::gmock_main - scratchcpp-render - scratchcpprender_mocks - ${QT_LIBS} -) - -add_test(penextension_test) -gtest_discover_tests(penextension_test) diff --git a/test/blocks/pen_blocks_test.cpp b/test/blocks/pen_blocks_test.cpp deleted file mode 100644 index 52d9712..0000000 --- a/test/blocks/pen_blocks_test.cpp +++ /dev/null @@ -1,1352 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../common.h" - -using namespace scratchcpprender; -using namespace libscratchcpp; - -using ::testing::Return; - -class PenBlocksTest : public testing::Test -{ - public: - void SetUp() override { m_section = std::make_unique(); } - - void addValueInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, const Value &value) const - { - auto input = std::make_shared(name, Input::Type::Shadow); - input->setPrimaryValue(value); - input->setInputId(id); - block->addInput(input); - } - - void addObscuredInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, std::shared_ptr valueBlock) const - { - auto input = std::make_shared(name, Input::Type::ObscuredShadow); - input->setValueBlock(valueBlock); - input->setInputId(id); - block->addInput(input); - } - - std::shared_ptr addNullInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id) const - { - auto input = std::make_shared(name, Input::Type::Shadow); - input->setInputId(id); - block->addInput(input); - - return input; - } - - void addDropdownInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, const std::string &selectedValue, std::shared_ptr valueBlock = nullptr) const - { - if (valueBlock) - addObscuredInput(block, name, id, valueBlock); - else { - auto input = addNullInput(block, name, id); - auto menu = std::make_shared(block->id() + "_menu", block->opcode() + "_menu"); - menu->setShadow(true); - input->setValueBlock(menu); - addDropdownField(menu, name, -1, selectedValue, -1); - } - } - - void addDropdownField(std::shared_ptr block, const std::string &name, int id, const std::string &value, int valueId) const - { - auto field = std::make_shared(name, value); - field->setFieldId(id); - field->setSpecialValueId(valueId); - block->addField(field); - } - - std::shared_ptr createNullBlock(const std::string &id) - { - std::shared_ptr block = std::make_shared(id, ""); - BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); }; - block->setCompileFunction(func); - - return block; - } - - std::unique_ptr m_section; - EngineMock m_engineMock; -}; - -TEST_F(PenBlocksTest, Name) -{ - ASSERT_EQ(m_section->name(), "Pen"); -} - -TEST_F(PenBlocksTest, CategoryVisible) -{ - ASSERT_TRUE(m_section->categoryVisible()); -} - -TEST_F(PenBlocksTest, RegisterBlocks) -{ - // Blocks - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_clear", &PenBlocks::compileClear)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_stamp", &PenBlocks::compileStamp)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_penDown", &PenBlocks::compilePenDown)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_penUp", &PenBlocks::compilePenUp)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenColorToColor", &PenBlocks::compileSetPenColorToColor)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenColorParamBy", &PenBlocks::compileChangePenColorParamBy)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenColorParamTo", &PenBlocks::compileSetPenColorParamTo)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenSizeBy", &PenBlocks::compileChangePenSizeBy)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenSizeTo", &PenBlocks::compileSetPenSizeTo)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenShadeBy", &PenBlocks::compileChangePenShadeBy)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenShadeToNumber", &PenBlocks::compileSetPenShadeToNumber)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenHueBy", &PenBlocks::compileChangePenHueBy)); - EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenHueToNumber", &PenBlocks::compileSetPenHueToNumber)); - - // Inputs - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COLOR", PenBlocks::COLOR)); - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COLOR_PARAM", PenBlocks::COLOR_PARAM)); - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "VALUE", PenBlocks::VALUE)); - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SIZE", PenBlocks::SIZE)); - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SHADE", PenBlocks::SHADE)); - EXPECT_CALL(m_engineMock, addInput(m_section.get(), "HUE", PenBlocks::HUE)); - - m_section->registerBlocks(&m_engineMock); -} - -TEST_F(PenBlocksTest, Clear) -{ - Compiler compiler(&m_engineMock); - - auto block = std::make_shared("a", "pen_clear"); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::clear)).WillOnce(Return(2)); - compiler.init(); - compiler.setBlock(block); - PenBlocks::compileClear(&compiler); - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_TRUE(compiler.constValues().empty()); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, Stamp) -{ - Compiler compiler(&m_engineMock); - - auto block = std::make_shared("a", "pen_stamp"); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::stamp)).WillOnce(Return(2)); - compiler.init(); - compiler.setBlock(block); - PenBlocks::compileStamp(&compiler); - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_TRUE(compiler.constValues().empty()); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, StampImpl) -{ - static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::stamp }; - - PenLayerMock penLayer; - PenLayer::addPenLayer(&m_engineMock, &penLayer); - RenderedTargetMock renderedTarget; - - // Test sprite - libscratchcpp::Sprite sprite; - SpriteModel spriteModel; - sprite.setInterface(&spriteModel); - spriteModel.setRenderedTarget(&renderedTarget); - - VirtualMachine vm1(&sprite, &m_engineMock, nullptr); - vm1.setBytecode(bytecode); - vm1.setFunctions(functions); - - EXPECT_CALL(penLayer, stamp(&renderedTarget)); - EXPECT_CALL(m_engineMock, requestRedraw()); - vm1.run(); - - ASSERT_EQ(vm1.registerCount(), 0); - - // Test stage - libscratchcpp::Stage stage; - StageModel stageModel; - stage.setInterface(&stageModel); - stageModel.setRenderedTarget(&renderedTarget); - - VirtualMachine vm2(&stage, &m_engineMock, nullptr); - vm2.setBytecode(bytecode); - vm2.setFunctions(functions); - - EXPECT_CALL(penLayer, stamp(&renderedTarget)); - EXPECT_CALL(m_engineMock, requestRedraw()); - vm2.run(); - - ASSERT_EQ(vm2.registerCount(), 0); -} - -TEST_F(PenBlocksTest, ClearImpl) -{ - static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::clear }; - - PenLayerMock penLayer; - PenLayer::addPenLayer(&m_engineMock, &penLayer); - - VirtualMachine vm(nullptr, &m_engineMock, nullptr); - vm.setBytecode(bytecode); - vm.setFunctions(functions); - - EXPECT_CALL(penLayer, clear()); - EXPECT_CALL(m_engineMock, requestRedraw()); - vm.run(); - - ASSERT_EQ(vm.registerCount(), 0); -} - -TEST_F(PenBlocksTest, PenDown) -{ - Compiler compiler(&m_engineMock); - - auto block = std::make_shared("a", "pen_penDown"); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::penDown)).WillOnce(Return(2)); - compiler.init(); - compiler.setBlock(block); - PenBlocks::compilePenDown(&compiler); - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_TRUE(compiler.constValues().empty()); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, PenDownImpl) -{ - static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::penDown }; - - SpriteModel model; - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode); - vm.setFunctions(functions); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_TRUE(model.penDown()); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_TRUE(model.penDown()); -} - -TEST_F(PenBlocksTest, PenUp) -{ - Compiler compiler(&m_engineMock); - - auto block = std::make_shared("a", "pen_penUp"); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::penUp)).WillOnce(Return(2)); - compiler.init(); - compiler.setBlock(block); - PenBlocks::compilePenUp(&compiler); - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_TRUE(compiler.constValues().empty()); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, PenUpImpl) -{ - static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::penUp }; - - SpriteModel model; - model.setPenDown(true); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode); - vm.setFunctions(functions); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_FALSE(model.penDown()); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_FALSE(model.penDown()); -} - -TEST_F(PenBlocksTest, SetPenColorToColor) -{ - Compiler compiler(&m_engineMock); - - // set pen color to ("#AABBCC") - auto block1 = std::make_shared("a", "pen_setPenColorToColor"); - addValueInput(block1, "COLOR", PenBlocks::COLOR, "#AABBCC"); - - // set pen color to (null block) - auto block2 = std::make_shared("b", "pen_setPenColorToColor"); - addObscuredInput(block2, "COLOR", PenBlocks::COLOR, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorToColor)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileSetPenColorToColor(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorToColor)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileSetPenColorToColor(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toString(), "#AABBCC"); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, SetPenColorToColorImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::setPenColorToColor }; - static Value constValues[] = { "#AABbCC", "#03F", "#FFGFFF", "#AABBCCDD", "FFFFFF", 1228097602, 255 }; - - SpriteModel model; - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(210, 42, 204))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(228, 255, 255))); - - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(359, 0, 0))); - - vm.reset(); - vm.setBytecode(bytecode4); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(359, 0, 0))); - - vm.reset(); - vm.setBytecode(bytecode5); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(359, 0, 0))); - - vm.reset(); - vm.setBytecode(bytecode6); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(162, 74, 72, 73))); - - vm.reset(); - vm.setBytecode(bytecode7); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(239, 255, 255))); -} - -TEST_F(PenBlocksTest, ChangePenColorParamBy) -{ - Compiler compiler(&m_engineMock); - - // change pen (color) by (34.6) - auto block1 = std::make_shared("a", "pen_changePenColorParamBy"); - addDropdownInput(block1, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "color"); - addValueInput(block1, "VALUE", PenBlocks::VALUE, 34.6); - - // change pen (saturation) by (46.8) - auto block2 = std::make_shared("b", "pen_changePenColorParamBy"); - addDropdownInput(block2, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "saturation"); - addValueInput(block2, "VALUE", PenBlocks::VALUE, 46.8); - - // change pen (brightness) by (0.45) - auto block3 = std::make_shared("c", "pen_changePenColorParamBy"); - addDropdownInput(block3, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "brightness"); - addValueInput(block3, "VALUE", PenBlocks::VALUE, 0.45); - - // change pen (transparency) by (89.06) - auto block4 = std::make_shared("d", "pen_changePenColorParamBy"); - addDropdownInput(block4, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "transparency"); - addValueInput(block4, "VALUE", PenBlocks::VALUE, 89.06); - - // change pen (invalid param) by (52.7) - auto block5 = std::make_shared("e", "pen_changePenColorParamBy"); - addDropdownInput(block5, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "invalid param"); - addValueInput(block5, "VALUE", PenBlocks::VALUE, 52.7); - - // change pen (null block) by (35.2) - auto block6 = std::make_shared("f", "pen_changePenColorParamBy"); - addDropdownInput(block6, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "", createNullBlock("g")); - addValueInput(block6, "VALUE", PenBlocks::VALUE, 35.2); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenColorBy)).WillOnce(Return(0)); - compiler.setBlock(block1); - PenBlocks::compileChangePenColorParamBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSaturationBy)).WillOnce(Return(1)); - compiler.setBlock(block2); - PenBlocks::compileChangePenColorParamBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenBrightnessBy)).WillOnce(Return(2)); - compiler.setBlock(block3); - PenBlocks::compileChangePenColorParamBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenTransparencyBy)).WillOnce(Return(3)); - compiler.setBlock(block4); - PenBlocks::compileChangePenColorParamBy(&compiler); - - compiler.setBlock(block5); - PenBlocks::compileChangePenColorParamBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenColorParamBy)).WillOnce(Return(4)); - compiler.setBlock(block6); - PenBlocks::compileChangePenColorParamBy(&compiler); - - compiler.end(); - - ASSERT_EQ( - compiler.bytecode(), - std::vector( - { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_CONST, 3, vm::OP_EXEC, 3, - vm::OP_NULL, vm::OP_CONST, 4, vm::OP_EXEC, 4, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues(), std::vector({ 34.6, 46.8, 0.45, 89.06, 35.2 })); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, ChangePenColorParamByImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode10[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 1, vm::OP_HALT }; - static unsigned int bytecode11[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 1, vm::OP_HALT }; - static unsigned int bytecode12[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT }; - static unsigned int bytecode13[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 2, vm::OP_HALT }; - static unsigned int bytecode14[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 3, vm::OP_HALT }; - static unsigned int bytecode15[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT }; - static unsigned int bytecode16[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 4, vm::OP_HALT }; - static unsigned int bytecode17[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 4, vm::OP_HALT }; - static BlockFunc - functions[] = { &PenBlocks::changePenColorParamBy, &PenBlocks::changePenColorBy, &PenBlocks::changePenSaturationBy, &PenBlocks::changePenBrightnessBy, &PenBlocks::changePenTransparencyBy }; - static Value constValues[] = { "color", "saturation", "brightness", "transparency", "invalid", 53.2, -120.8 }; - - SpriteModel model; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - // color - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(71, 255, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(263, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 255, 255, 150))); - - // saturation - model.penState().saturation = 32.4; - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 218, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode4); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 255, 150))); - - // brightness - model.penState().brightness = 12.5; - vm.reset(); - vm.setBytecode(bytecode5); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 167, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode6); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 150))); - - // transparency - model.penState().transparency = 6.28; - vm.reset(); - vm.setBytecode(bytecode7); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 103))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 0))); - - vm.reset(); - vm.setBytecode(bytecode8); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 255))); - - // invalid parameter - vm.reset(); - vm.setBytecode(bytecode9); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 255))); - - // color (optimized) - model.penState() = PenState(); - model.penState().transparency = 100 * (1 - 150 / 255.0); - vm.reset(); - vm.setBytecode(bytecode10); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(71, 255, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(263, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode11); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 255, 255, 150))); - - // saturation (optimized) - model.penState().saturation = 32.4; - vm.reset(); - vm.setBytecode(bytecode12); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 218, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode13); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 255, 150))); - - // brightness (optimized) - model.penState().brightness = 12.5; - vm.reset(); - vm.setBytecode(bytecode14); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 167, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode15); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 150))); - - // transparency (optimized) - model.penState().transparency = 6.28; - vm.reset(); - vm.setBytecode(bytecode16); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 103))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 0))); - - vm.reset(); - vm.setBytecode(bytecode17); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(188, 0, 0, 255))); -} - -TEST_F(PenBlocksTest, SetPenColorParamTo) -{ - Compiler compiler(&m_engineMock); - - // set pen (color) to (34.6) - auto block1 = std::make_shared("a", "pen_setPenColorParamTo"); - addDropdownInput(block1, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "color"); - addValueInput(block1, "VALUE", PenBlocks::VALUE, 34.6); - - // set pen (saturation) to (46.8) - auto block2 = std::make_shared("b", "pen_setPenColorParamTo"); - addDropdownInput(block2, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "saturation"); - addValueInput(block2, "VALUE", PenBlocks::VALUE, 46.8); - - // set pen (brightness) to (0.45) - auto block3 = std::make_shared("c", "pen_setPenColorParamTo"); - addDropdownInput(block3, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "brightness"); - addValueInput(block3, "VALUE", PenBlocks::VALUE, 0.45); - - // set pen (transparency) to (89.06) - auto block4 = std::make_shared("d", "pen_setPenColorParamTo"); - addDropdownInput(block4, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "transparency"); - addValueInput(block4, "VALUE", PenBlocks::VALUE, 89.06); - - // set pen (invalid param) to (52.7) - auto block5 = std::make_shared("e", "pen_setPenColorParamTo"); - addDropdownInput(block5, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "invalid param"); - addValueInput(block5, "VALUE", PenBlocks::VALUE, 52.7); - - // set pen (null block) to (35.2) - auto block6 = std::make_shared("f", "pen_setPenColorParamTo"); - addDropdownInput(block6, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "", createNullBlock("g")); - addValueInput(block6, "VALUE", PenBlocks::VALUE, 35.2); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorTo)).WillOnce(Return(0)); - compiler.setBlock(block1); - PenBlocks::compileSetPenColorParamTo(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSaturationTo)).WillOnce(Return(1)); - compiler.setBlock(block2); - PenBlocks::compileSetPenColorParamTo(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenBrightnessTo)).WillOnce(Return(2)); - compiler.setBlock(block3); - PenBlocks::compileSetPenColorParamTo(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenTransparencyTo)).WillOnce(Return(3)); - compiler.setBlock(block4); - PenBlocks::compileSetPenColorParamTo(&compiler); - - compiler.setBlock(block5); - PenBlocks::compileSetPenColorParamTo(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorParamTo)).WillOnce(Return(4)); - compiler.setBlock(block6); - PenBlocks::compileSetPenColorParamTo(&compiler); - - compiler.end(); - - ASSERT_EQ( - compiler.bytecode(), - std::vector( - { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_CONST, 3, vm::OP_EXEC, 3, - vm::OP_NULL, vm::OP_CONST, 4, vm::OP_EXEC, 4, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues(), std::vector({ 34.6, 46.8, 0.45, 89.06, 35.2 })); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, SetPenColorParamToImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode10[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode11[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode12[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode13[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode14[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 1, vm::OP_HALT }; - static unsigned int bytecode15[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 1, vm::OP_HALT }; - static unsigned int bytecode16[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 1, vm::OP_HALT }; - static unsigned int bytecode17[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT }; - static unsigned int bytecode18[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 2, vm::OP_HALT }; - static unsigned int bytecode19[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 2, vm::OP_HALT }; - static unsigned int bytecode20[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 3, vm::OP_HALT }; - static unsigned int bytecode21[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT }; - static unsigned int bytecode22[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 3, vm::OP_HALT }; - static unsigned int bytecode23[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 4, vm::OP_HALT }; - static unsigned int bytecode24[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 4, vm::OP_HALT }; - static unsigned int bytecode25[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 4, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::setPenColorParamTo, &PenBlocks::setPenColorTo, &PenBlocks::setPenSaturationTo, &PenBlocks::setPenBrightnessTo, &PenBlocks::setPenTransparencyTo }; - static Value constValues[] = { "color", "saturation", "brightness", "transparency", "invalid", 53.2, -234.9, 287.1 }; - - SpriteModel model; - model.penState().color = 78.6; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - // color - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(191, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(234, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // saturation - model.penState().saturation = 32.4; - vm.reset(); - vm.setBytecode(bytecode4); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 135, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode5); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 0, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode6); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // brightness - model.penState().brightness = 12.5; - vm.reset(); - vm.setBytecode(bytecode7); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 135, 150))); - - vm.reset(); - vm.setBytecode(bytecode8); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 0, 150))); - - vm.reset(); - vm.setBytecode(bytecode9); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // transparency - model.penState().transparency = 12.5; - vm.reset(); - vm.setBytecode(bytecode10); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 119))); - - vm.reset(); - vm.setBytecode(bytecode11); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 255))); - - vm.reset(); - vm.setBytecode(bytecode12); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 0))); - - // invalid parameter - vm.reset(); - vm.setBytecode(bytecode13); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 0))); - - // color (optimized) - model.penState() = PenState(); - model.penState().color = 78.6; - model.penState().transparency = 100 * (1 - 150 / 255.0); - vm.reset(); - vm.setBytecode(bytecode14); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(191, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode15); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(234, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode16); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // saturation (optimized) - model.penState().saturation = 32.4; - vm.reset(); - vm.setBytecode(bytecode17); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 135, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode18); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 0, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode19); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // brightness (optimized) - model.penState().brightness = 12.5; - vm.reset(); - vm.setBytecode(bytecode20); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 135, 150))); - - vm.reset(); - vm.setBytecode(bytecode21); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 0, 150))); - - vm.reset(); - vm.setBytecode(bytecode22); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 150))); - - // transparency (optimized) - model.penState().transparency = 12.5; - vm.reset(); - vm.setBytecode(bytecode23); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 119))); - - vm.reset(); - vm.setBytecode(bytecode24); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 255))); - - vm.reset(); - vm.setBytecode(bytecode25); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(313, 255, 255, 0))); -} - -TEST_F(PenBlocksTest, ChangePenSizeBy) -{ - Compiler compiler(&m_engineMock); - - // change pen size by (4.5) - auto block1 = std::make_shared("a", "pen_changePenSizeBy"); - addValueInput(block1, "SIZE", PenBlocks::SIZE, 4.5); - - // change pen size by (null block) - auto block2 = std::make_shared("b", "pen_changePenSizeBy"); - addObscuredInput(block2, "SIZE", PenBlocks::SIZE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSizeBy)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileChangePenSizeBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSizeBy)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileChangePenSizeBy(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, ChangePenSizeByImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::changePenSizeBy }; - static Value constValues[] = { 511.5, -650.08 }; - - SpriteModel model; - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 512.5); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 1024); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 1200); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 549.92); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 1); -} - -TEST_F(PenBlocksTest, SetPenSizeTo) -{ - Compiler compiler(&m_engineMock); - - // set pen size to (51.46) - auto block1 = std::make_shared("a", "pen_setPenSizeTo"); - addValueInput(block1, "SIZE", PenBlocks::SIZE, 51.46); - - // set pen size to (null block) - auto block2 = std::make_shared("b", "pen_setPenSizeTo"); - addObscuredInput(block2, "SIZE", PenBlocks::SIZE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSizeTo)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileSetPenSizeTo(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSizeTo)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileSetPenSizeTo(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 51.46); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, SetPenSizeToImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::setPenSizeTo }; - static Value constValues[] = { 511.5, -650.08, 1500 }; - - SpriteModel model; - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 511.5); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 1); - - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().diameter, 1200); -} - -TEST_F(PenBlocksTest, ChangePenShadeBy) -{ - Compiler compiler(&m_engineMock); - - // change pen shade by (4.5) - auto block1 = std::make_shared("a", "pen_changePenShadeBy"); - addValueInput(block1, "SHADE", PenBlocks::SHADE, 4.5); - - // change pen shade by (null block) - auto block2 = std::make_shared("b", "pen_changePenShadeBy"); - addObscuredInput(block2, "SHADE", PenBlocks::SHADE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenShadeBy)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileChangePenShadeBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenShadeBy)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileChangePenShadeBy(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, ChangePenShadeByImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::changePenShadeBy }; - static Value constValues[] = { 134.09, -124.45 }; - - SpriteModel model; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 255, 110, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 119, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 247, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 162, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 255, 55, 150))); -} - -TEST_F(PenBlocksTest, SetPenShadeToNumber) -{ - Compiler compiler(&m_engineMock); - - // set pen shade to (4.5) - auto block1 = std::make_shared("a", "pen_setPenShadeToNumber"); - addValueInput(block1, "SHADE", PenBlocks::SHADE, 4.5); - - // set pen shade to (null block) - auto block2 = std::make_shared("b", "pen_setPenShadeToNumber"); - addObscuredInput(block2, "SHADE", PenBlocks::SHADE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenShadeToNumber)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileSetPenShadeToNumber(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenShadeToNumber)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileSetPenShadeToNumber(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, SetPenShadeToNumberImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::setPenShadeToNumber }; - static Value constValues[] = { 125.7, -114.09, 489.4 }; - - SpriteModel model; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 148, 253, 150))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 102, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(240, 89, 255, 150))); -} - -TEST_F(PenBlocksTest, ChangePenHueBy) -{ - Compiler compiler(&m_engineMock); - - // change pen hue by (4.5) - auto block1 = std::make_shared("a", "pen_changePenHueBy"); - addValueInput(block1, "HUE", PenBlocks::HUE, 4.5); - - // change pen hue by (null block) - auto block2 = std::make_shared("b", "pen_changePenHueBy"); - addObscuredInput(block2, "HUE", PenBlocks::HUE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenHueBy)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileChangePenHueBy(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenHueBy)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileChangePenHueBy(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, ChangePenHueByImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::changePenHueBy }; - static Value constValues[] = { 125.7, -114.09 }; - - SpriteModel model; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(106, 255, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(332, 255, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(199, 255, 255, 150))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(353, 255, 255, 150))); - - vm.reset(); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(148, 255, 255, 150))); -} - -TEST_F(PenBlocksTest, SetPenHueToNumber) -{ - Compiler compiler(&m_engineMock); - - // set pen hue to (54.09) - auto block1 = std::make_shared("a", "pen_setPenHueToNumber"); - addValueInput(block1, "HUE", PenBlocks::HUE, 54.09); - - // set pen hue to (null block) - auto block2 = std::make_shared("b", "pen_setPenHueToNumber"); - addObscuredInput(block2, "HUE", PenBlocks::HUE, createNullBlock("c")); - - compiler.init(); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenHueToNumber)).WillOnce(Return(2)); - compiler.setBlock(block1); - PenBlocks::compileSetPenHueToNumber(&compiler); - - EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenHueToNumber)).WillOnce(Return(2)); - compiler.setBlock(block2); - PenBlocks::compileSetPenHueToNumber(&compiler); - - compiler.end(); - - ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); - ASSERT_EQ(compiler.constValues().size(), 1); - ASSERT_EQ(compiler.constValues()[0].toDouble(), 54.09); - ASSERT_TRUE(compiler.variables().empty()); - ASSERT_TRUE(compiler.lists().empty()); -} - -TEST_F(PenBlocksTest, SetPenHueToNumberImpl) -{ - static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; - static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; - static BlockFunc functions[] = { &PenBlocks::setPenHueToNumber }; - static Value constValues[] = { 125.7, -114.09, 489.4 }; - - SpriteModel model; - model.penState().transparency = 100 * (1 - 150 / 255.0); - Sprite sprite; - sprite.setInterface(&model); - - VirtualMachine vm(&sprite, &m_engineMock, nullptr); - vm.setBytecode(bytecode1); - vm.setFunctions(functions); - vm.setConstValues(constValues); - - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(226, 255, 255, 255))); - - vm.reset(); - vm.setBytecode(bytecode2); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(154, 255, 255, 255))); - - vm.reset(); - vm.setBytecode(bytecode3); - vm.run(); - ASSERT_EQ(vm.registerCount(), 0); - ASSERT_EQ(model.penAttributes().color, QNanoColor::fromQColor(QColor::fromHsv(160, 255, 255, 255))); -} diff --git a/test/blocks/penextension_test.cpp b/test/blocks/penextension_test.cpp deleted file mode 100644 index 009a497..0000000 --- a/test/blocks/penextension_test.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include - -#include "../common.h" - -using namespace scratchcpprender; -using namespace libscratchcpp; - -using ::testing::WithArgs; -using ::testing::Invoke; -using ::testing::_; - -TEST(PenExtensionTest, Name) -{ - PenExtension ext; - ASSERT_EQ(ext.name(), "pen"); -} - -TEST(PenExtensionTest, Description) -{ - PenExtension ext; - ASSERT_EQ(ext.description(), "Pen extension"); -} - -TEST(PenExtensionTest, IncludeByDefault) -{ - PenExtension ext; - ASSERT_FALSE(ext.includeByDefault()); -} - -TEST(PenExtensionTest, RegisterSections) -{ - PenExtension ext; - EngineMock engine; - - EXPECT_CALL(engine, registerSection(_)).WillOnce(WithArgs<0>(Invoke([](std::shared_ptr section) { ASSERT_TRUE(dynamic_cast(section.get())); }))); - ext.registerSections(&engine); -} diff --git a/test/color_effects.png b/test/color_effects.png index c18e4a3..5a34441 100644 Binary files a/test/color_effects.png and b/test/color_effects.png differ diff --git a/test/common.h b/test/common.h index 847aa84..36f354b 100644 --- a/test/common.h +++ b/test/common.h @@ -28,7 +28,7 @@ double fuzzyCompareImages(const QImage &a, const QImage &b) for (y = 0; y < a.height(); y++) { for (x = 0; x < a.width(); x++) { - if (a.pixel(x, y) != b.pixel(x, y)) + if (a.pixelColor(x, y).rgba() != b.pixelColor(x, y).rgba()) c++; } } diff --git a/test/effecttransform/CMakeLists.txt b/test/effecttransform/CMakeLists.txt new file mode 100644 index 0000000..89e9206 --- /dev/null +++ b/test/effecttransform/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + effecttransform_test + effecttransform_test.cpp +) + +target_link_libraries( + effecttransform_test + GTest::gtest_main + scratchcpp-render + qnanopainter +) + +add_test(effecttransform_test) +gtest_discover_tests(effecttransform_test) diff --git a/test/effecttransform/effecttransform_test.cpp b/test/effecttransform/effecttransform_test.cpp new file mode 100644 index 0000000..d9ee128 --- /dev/null +++ b/test/effecttransform/effecttransform_test.cpp @@ -0,0 +1,195 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; + +class EffectTransformTest : public testing::Test +{ + public: + void SetUp() override { } + + std::unordered_map m_effects; +}; + +TEST_F(EffectTransformTest, NoEffect) +{ + QRgb color = qRgba(0, 0, 0, 0); + auto mask = ShaderManager::Effect::NoEffect; + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(0, 255, 255, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(255, 255, 255, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + QVector2D dst; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.5, -0.3), dst); + ASSERT_EQ(dst, QVector2D(0.5, -0.3)); +} + +TEST_F(EffectTransformTest, ColorEffect) +{ + // 100 + m_effects[ShaderManager::Effect::Color] = 100; + auto mask = ShaderManager::Effect::Color; + QRgb color = qRgba(0, 0, 0, 0); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(0, 255, 255)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(128, 100, 100, 128)); + + // 175 + m_effects[ShaderManager::Effect::Color] = 175; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(255, 0, 191)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(100, 128, 107, 128)); +} + +TEST_F(EffectTransformTest, BrightnessEffect) +{ + // -100 + m_effects[ShaderManager::Effect::Brightness] = -100; + auto mask = ShaderManager::Effect::Brightness; + QRgb color = qRgba(0, 0, 0, 0); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(0, 0, 0)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(0, 0, 0, 128)); + + // -50 + m_effects[ShaderManager::Effect::Brightness] = -50; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(127, 0, 0)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(36, 64, 64, 128)); + + // 50 + m_effects[ShaderManager::Effect::Brightness] = 50; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(255, 127, 127)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(128, 128, 128, 128)); + + // 100 + m_effects[ShaderManager::Effect::Brightness] = 100; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgb(255, 255, 255)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(128, 128, 128, 128)); +} + +TEST_F(EffectTransformTest, GhostEffect) +{ + // 25 + m_effects[ShaderManager::Effect::Ghost] = 25; + auto mask = ShaderManager::Effect::Ghost; + QRgb color = qRgba(0, 0, 0, 0); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), color); + + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(191, 0, 0, 191)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(75, 191, 150, 96)); + + // 50 + m_effects[ShaderManager::Effect::Ghost] = 50; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(128, 0, 0, 128)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(50, 128, 100, 64)); + + // 100 + m_effects[ShaderManager::Effect::Ghost] = 100; + color = qRgba(255, 0, 0, 255); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(0, 0, 0, 0)); + + color = qRgba(100, 255, 200, 128); + ASSERT_EQ(EffectTransform::transformColor(mask, m_effects, color), qRgba(0, 0, 0, 0)); +} + +TEST_F(EffectTransformTest, FisheyeEffect) +{ + // 50 + m_effects[ShaderManager::Effect::Fisheye] = 50; + auto mask = ShaderManager::Effect::Fisheye; + QVector2D dst; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.51, 0.49), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.502f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.498f); + + // 200 + m_effects[ShaderManager::Effect::Fisheye] = 200; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.4, 0.68), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.483f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.531f); +} + +TEST_F(EffectTransformTest, WhirlEffect) +{ + // 50 + m_effects[ShaderManager::Effect::Whirl] = 50; + auto mask = ShaderManager::Effect::Whirl; + QVector2D dst; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.51, 0.49), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.499f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.486f); + + // 200 + m_effects[ShaderManager::Effect::Whirl] = 200; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.4, 0.68), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.633f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.657f); +} + +TEST_F(EffectTransformTest, PixelateEffect) +{ + // 5 + m_effects[ShaderManager::Effect::Pixelate] = 5; + auto mask = ShaderManager::Effect::Pixelate; + QVector2D dst; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.51, 0.05), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.75f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.25f); + + // 20 + m_effects[ShaderManager::Effect::Pixelate] = 20; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.97, 0.68), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 1.0f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 1.0f); +} + +TEST_F(EffectTransformTest, MosaicEffect) +{ + // 50 + m_effects[ShaderManager::Effect::Mosaic] = 50; + auto mask = ShaderManager::Effect::Mosaic; + QVector2D dst; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.75, 0.25), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.5f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.5f); + + // 200 + m_effects[ShaderManager::Effect::Mosaic] = 200; + EffectTransform::transformPoint(mask, m_effects, QSize(), QVector2D(0.8, 0.68), dst); + ASSERT_EQ(std::round(dst.x() * 1000.0f) / 1000.0f, 0.8f); + ASSERT_EQ(std::round(dst.y() * 1000.0f) / 1000.0f, 0.28f); +} diff --git a/test/graphicseffect/graphicseffect_test.cpp b/test/graphicseffect/graphicseffect_test.cpp index 7ea7338..4820439 100644 --- a/test/graphicseffect/graphicseffect_test.cpp +++ b/test/graphicseffect/graphicseffect_test.cpp @@ -27,3 +27,58 @@ TEST(GraphicsEffectTest, Constructor) ASSERT_EQ(iface->name(), "brightness"); } } + +TEST(GraphicsEffectTest, Clamp) +{ + { + GraphicsEffect effect(ShaderManager::Effect::Color, "color"); + ASSERT_EQ(effect.clamp(-500), -500); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(500), 500); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Brightness, "brightness"); + ASSERT_EQ(effect.clamp(-125), -100); + ASSERT_EQ(effect.clamp(-100), -100); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(100), 100); + ASSERT_EQ(effect.clamp(125), 100); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Ghost, "ghost"); + ASSERT_EQ(effect.clamp(-50), 0); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(100), 100); + ASSERT_EQ(effect.clamp(125), 100); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Fisheye, "fisheye"); + ASSERT_EQ(effect.clamp(-5000), -5000); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(5000), 5000); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Whirl, "whirl"); + ASSERT_EQ(effect.clamp(-5000), -5000); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(5000), 5000); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Pixelate, "pixelate"); + ASSERT_EQ(effect.clamp(-5000), -5000); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(5000), 5000); + } + + { + GraphicsEffect effect(ShaderManager::Effect::Mosaic, "mosaic"); + ASSERT_EQ(effect.clamp(-5000), -5000); + ASSERT_EQ(effect.clamp(0), 0); + ASSERT_EQ(effect.clamp(5000), 5000); + } +} diff --git a/test/lines.png b/test/lines.png index 245885e..5792818 100644 Binary files a/test/lines.png and b/test/lines.png differ diff --git a/test/lines_hq.png b/test/lines_hq.png index c78d1d3..2cdc7e7 100644 Binary files a/test/lines_hq.png and b/test/lines_hq.png differ diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h index 2d9aaa7..8f63633 100644 --- a/test/mocks/enginemock.h +++ b/test/mocks/enginemock.h @@ -8,7 +8,7 @@ using namespace libscratchcpp; namespace scratchcpprender { -using ScriptMap = std::unordered_map, std::shared_ptr