Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9f3d979
Prepare code for pen blocks
adazem009 Mar 21, 2026
8a09468
Switch to shared library
adazem009 Mar 21, 2026
98b0ead
Implement pen_clear block
adazem009 Mar 21, 2026
52855ea
Implement pen_stamp block
adazem009 Mar 21, 2026
fcb8cf6
Implement pen_penDown block
adazem009 Mar 21, 2026
c0f7013
Implement pen_penUp block
adazem009 Mar 21, 2026
d6def8f
Fix pen blocks extension name
adazem009 Mar 21, 2026
887823d
Fix precision issues in ShaderManager test suite
adazem009 Mar 22, 2026
77b4d1c
Refactor pen frame state management
adazem009 Mar 22, 2026
381b4c8
PenLayer: Remove unused variables
adazem009 Mar 22, 2026
8e8abc1
PenLayer: Drop stamp FBO
adazem009 Mar 22, 2026
db08870
Move stamp rendering to RenderedTarget
adazem009 Mar 22, 2026
f3c4918
RenderedTarget: Cache matrices
adazem009 Mar 22, 2026
ab64766
ShaderManager: Make setUniforms() static
adazem009 Mar 22, 2026
9c8640d
RenderedTarget: Cache shader program
adazem009 Mar 22, 2026
2f627e6
Implement pen_setPenColorToColor block
adazem009 Mar 27, 2026
3919ddc
Optimize color constants in set pen color block
adazem009 Mar 27, 2026
7baf07a
Implement pen_changePenColorParamBy block
adazem009 Mar 28, 2026
9da34e4
Optimize constant param names in change color param block
adazem009 Mar 28, 2026
ed691ee
Implement pen_setPenColorParamTo block
adazem009 Mar 28, 2026
544f4d3
Add missing pen block stage test cases
adazem009 Mar 28, 2026
7efdd13
Implement pen_changePenSizeBy block
adazem009 Mar 28, 2026
d153557
Implement pen_setPenSizeTo block
adazem009 Mar 28, 2026
e7b3ea7
Implement pen_changePenShadeBy block
adazem009 Mar 28, 2026
2773b4b
Implement pen_setPenShadeToNumber block
adazem009 Mar 28, 2026
0087806
Implement pen_changePenHueBy block
adazem009 Mar 28, 2026
a005131
Implement pen_setPenHueToNumber block
adazem009 Mar 28, 2026
3ea17ae
ProjectLoader: Add missing pen layer null check
adazem009 Mar 28, 2026
fb92ade
PenLayer: Make sure FBO is bound when clearing in setEngine()
adazem009 Mar 28, 2026
db269be
Add missing include guard to pen blocks
adazem009 Mar 28, 2026
b27b72c
Refactor shader program cache
adazem009 Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement pen_changePenShadeBy block
  • Loading branch information
adazem009 committed Mar 28, 2026
commit e7b3ea70402fce30992f529a4bc62cc7c31cc8c3
68 changes: 68 additions & 0 deletions src/blocks/penblocks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ void PenBlocks::registerBlocks(IEngine *engine)
engine->addCompileFunction(this, "pen_setPenColorParamTo", &compileSetPenColorParamTo);
engine->addCompileFunction(this, "pen_changePenSizeBy", &compileChangePenSizeBy);
engine->addCompileFunction(this, "pen_setPenSizeTo", &compileSetPenSizeTo);
engine->addCompileFunction(this, "pen_changePenShadeBy", &compileChangePenShadeBy);
}

CompilerValue *PenBlocks::compileClear(Compiler *compiler)
Expand Down Expand Up @@ -198,6 +199,14 @@ CompilerValue *PenBlocks::compileSetPenSizeTo(Compiler *compiler)
return nullptr;
}

CompilerValue *PenBlocks::compileChangePenShadeBy(Compiler *compiler)
{
CompilerValue *shade = compiler->addInput("SHADE");
CompilerValue *change = compiler->addConstValue(true);
compiler->addTargetFunctionCall("pen_set_or_change_pen_shade", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { shade, change });
return nullptr;
}

static TargetModel *getTargetModel(Target *target)
{
if (target->isStage()) {
Expand Down Expand Up @@ -337,3 +346,62 @@ BLOCK_EXPORT void pen_setPenSizeTo(Target *target, double value)
PenAttributes &penAttributes = getTargetModel(target)->penAttributes();
penAttributes.diameter = std::clamp(value, PEN_SIZE_MIN, PEN_SIZE_MAX);
}

static QRgb mix_rgb(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<int>(((fraction0 * qRed(rgb0)) + (fraction1 * qRed(rgb1)))) & 255;
const int g = static_cast<int>(((fraction0 * qGreen(rgb0)) + (fraction1 * qGreen(rgb1)))) & 255;
const int b = static_cast<int>(((fraction0 * qBlue(rgb0)) + (fraction1 * qBlue(rgb1)))) & 255;
return qRgb(r, g, b);
}

inline void legacy_update_pen_color(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 = mix_rgb(0, rgb, (10 + shade) / 60);
else
rgb = mix_rgb(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();
}

BLOCK_EXPORT void pen_set_or_change_pen_shade(Target *target, double shade, bool change)
{
// https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L718-L730
PenState &penState = getTargetModel(target)->penState();

if (change)
shade += penState.shade;

// Wrap clamp the new shade value the way Scratch 2 did
const double hi = 200.0;
shade = fmod(shade, hi);

if (shade < 0)
shade += hi;

// And store the shade that was used to compute this new color for later use
penState.shade = shade;

legacy_update_pen_color(penState);
}
1 change: 1 addition & 0 deletions src/blocks/penblocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PenBlocks : public libscratchcpp::IExtension
static libscratchcpp::CompilerValue *compileSetPenColorParamTo(libscratchcpp::Compiler *compiler);
static libscratchcpp::CompilerValue *compileChangePenSizeBy(libscratchcpp::Compiler *compiler);
static libscratchcpp::CompilerValue *compileSetPenSizeTo(libscratchcpp::Compiler *compiler);
static libscratchcpp::CompilerValue *compileChangePenShadeBy(libscratchcpp::Compiler *compiler);
};

} // namespace scratchcpprender
144 changes: 144 additions & 0 deletions test/blocks/pen_blocks_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2202,3 +2202,147 @@ TEST_F(PenBlocksTest, SetPenSizeTo_Stage)
thread->run();
ASSERT_EQ(model.penAttributes().diameter, 511.5);
}

TEST_F(PenBlocksTest, ChangePenShadeBy)
{
auto sprite = std::make_shared<Sprite>();
sprite->setEngine(&m_engineMock);

RenderedTarget renderedTarget;
SpriteModel model;
model.init(sprite.get());
model.setRenderedTarget(&renderedTarget);
sprite->setInterface(&model);

ScriptBuilder builder(m_extension.get(), m_engine, sprite);
builder.addBlock("pen_changePenShadeBy");
builder.addValueInput("SHADE", 49.2);

PenState &penState = model.penState();
penState.color = 60;
penState.saturation = 90;
penState.brightness = 75;
penState.shade = 13.8;
penState.updateColor();

auto original = model.penAttributes().color;

auto thread = buildScript(builder, sprite.get());

EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
thread->run();

EXPECT_EQ(model.penAttributes().color.red(), 55);
EXPECT_EQ(model.penAttributes().color.green(), 135);
EXPECT_EQ(model.penAttributes().color.blue(), 255);
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
EXPECT_EQ(model.penState().shade, 63);
}

TEST_F(PenBlocksTest, ChangePenShadeBy_OutOfRange)
{
auto sprite = std::make_shared<Sprite>();
sprite->setEngine(&m_engineMock);

RenderedTarget renderedTarget;
SpriteModel model;
model.init(sprite.get());
model.setRenderedTarget(&renderedTarget);
sprite->setInterface(&model);

ScriptBuilder builder(m_extension.get(), m_engine, sprite);
builder.addBlock("pen_changePenShadeBy");
builder.addValueInput("SHADE", 189.6);

PenState &penState = model.penState();
penState.color = 60;
penState.saturation = 90;
penState.brightness = 75;
penState.shade = 13.8;
penState.updateColor();

auto original = model.penAttributes().color;

auto thread = buildScript(builder, sprite.get());

EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
thread->run();

EXPECT_EQ(model.penAttributes().color.red(), 0);
EXPECT_EQ(model.penAttributes().color.green(), 22);
EXPECT_EQ(model.penAttributes().color.blue(), 56);
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
EXPECT_EQ(std::round(model.penState().shade * 100) / 100, 3.4);
}

TEST_F(PenBlocksTest, ChangePenShadeBy_OutOfRange_Negative)
{
auto sprite = std::make_shared<Sprite>();
sprite->setEngine(&m_engineMock);

RenderedTarget renderedTarget;
SpriteModel model;
model.init(sprite.get());
model.setRenderedTarget(&renderedTarget);
sprite->setInterface(&model);

ScriptBuilder builder(m_extension.get(), m_engine, sprite);
builder.addBlock("pen_changePenShadeBy");
builder.addValueInput("SHADE", -25.3);

PenState &penState = model.penState();
penState.color = 60;
penState.saturation = 90;
penState.brightness = 75;
penState.shade = 13.8;
penState.updateColor();

auto original = model.penAttributes().color;

auto thread = buildScript(builder, sprite.get());

EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
thread->run();

EXPECT_EQ(model.penAttributes().color.red(), 0);
EXPECT_EQ(model.penAttributes().color.green(), 36);
EXPECT_EQ(model.penAttributes().color.blue(), 91);
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
EXPECT_EQ(model.penState().shade, 188.5);
}

TEST_F(PenBlocksTest, ChangePenShadeBy_Stage)
{
auto stage = std::make_shared<Stage>();
stage->setEngine(&m_engineMock);

RenderedTarget renderedTarget;
StageModel model;
model.init(stage.get());
model.setRenderedTarget(&renderedTarget);
stage->setInterface(&model);

ScriptBuilder builder(m_extension.get(), m_engine, stage);
builder.addBlock("pen_changePenShadeBy");
builder.addValueInput("SHADE", 49.2);

PenState &penState = model.penState();
penState.color = 60;
penState.saturation = 90;
penState.brightness = 75;
penState.shade = 13.8;
penState.updateColor();

auto original = model.penAttributes().color;

auto thread = buildScript(builder, stage.get());

EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
thread->run();

EXPECT_EQ(model.penAttributes().color.red(), 55);
EXPECT_EQ(model.penAttributes().color.green(), 135);
EXPECT_EQ(model.penAttributes().color.blue(), 255);
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
EXPECT_EQ(model.penState().shade, 63);
}
Loading