Skip to content

Commit e7b3ea7

Browse files
committed
Implement pen_changePenShadeBy block
1 parent d153557 commit e7b3ea7

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

src/blocks/penblocks.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ void PenBlocks::registerBlocks(IEngine *engine)
8888
engine->addCompileFunction(this, "pen_setPenColorParamTo", &compileSetPenColorParamTo);
8989
engine->addCompileFunction(this, "pen_changePenSizeBy", &compileChangePenSizeBy);
9090
engine->addCompileFunction(this, "pen_setPenSizeTo", &compileSetPenSizeTo);
91+
engine->addCompileFunction(this, "pen_changePenShadeBy", &compileChangePenShadeBy);
9192
}
9293

9394
CompilerValue *PenBlocks::compileClear(Compiler *compiler)
@@ -198,6 +199,14 @@ CompilerValue *PenBlocks::compileSetPenSizeTo(Compiler *compiler)
198199
return nullptr;
199200
}
200201

202+
CompilerValue *PenBlocks::compileChangePenShadeBy(Compiler *compiler)
203+
{
204+
CompilerValue *shade = compiler->addInput("SHADE");
205+
CompilerValue *change = compiler->addConstValue(true);
206+
compiler->addTargetFunctionCall("pen_set_or_change_pen_shade", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { shade, change });
207+
return nullptr;
208+
}
209+
201210
static TargetModel *getTargetModel(Target *target)
202211
{
203212
if (target->isStage()) {
@@ -337,3 +346,62 @@ BLOCK_EXPORT void pen_setPenSizeTo(Target *target, double value)
337346
PenAttributes &penAttributes = getTargetModel(target)->penAttributes();
338347
penAttributes.diameter = std::clamp(value, PEN_SIZE_MIN, PEN_SIZE_MAX);
339348
}
349+
350+
static QRgb mix_rgb(QRgb rgb0, QRgb rgb1, double fraction1)
351+
{
352+
// https://github.com/scratchfoundation/scratch-vm/blob/a4f095db5e03e072ba222fe721eeeb543c9b9c15/src/util/color.js#L192-L201
353+
// https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/util/Color.as#L75-L89
354+
if (fraction1 <= 0)
355+
return rgb0;
356+
357+
if (fraction1 >= 1)
358+
return rgb1;
359+
360+
const double fraction0 = 1 - fraction1;
361+
const int r = static_cast<int>(((fraction0 * qRed(rgb0)) + (fraction1 * qRed(rgb1)))) & 255;
362+
const int g = static_cast<int>(((fraction0 * qGreen(rgb0)) + (fraction1 * qGreen(rgb1)))) & 255;
363+
const int b = static_cast<int>(((fraction0 * qBlue(rgb0)) + (fraction1 * qBlue(rgb1)))) & 255;
364+
return qRgb(r, g, b);
365+
}
366+
367+
inline void legacy_update_pen_color(PenState &penState)
368+
{
369+
// https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L750-L767
370+
// Create the new color in RGB using the scratch 2 "shade" model
371+
QRgb rgb = QColor::fromHsvF(penState.color / 100, 1, 1).rgb();
372+
const double shade = (penState.shade > 100) ? 200 - penState.shade : penState.shade;
373+
374+
if (shade < 50)
375+
rgb = mix_rgb(0, rgb, (10 + shade) / 60);
376+
else
377+
rgb = mix_rgb(rgb, 0xFFFFFF, (shade - 50) / 60);
378+
379+
// Update the pen state according to new color
380+
QColor hsv = QColor::fromRgb(rgb).toHsv();
381+
penState.color = 100 * hsv.hueF();
382+
penState.saturation = 100 * hsv.saturationF();
383+
penState.brightness = 100 * hsv.valueF();
384+
385+
penState.updateColor();
386+
}
387+
388+
BLOCK_EXPORT void pen_set_or_change_pen_shade(Target *target, double shade, bool change)
389+
{
390+
// https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L718-L730
391+
PenState &penState = getTargetModel(target)->penState();
392+
393+
if (change)
394+
shade += penState.shade;
395+
396+
// Wrap clamp the new shade value the way Scratch 2 did
397+
const double hi = 200.0;
398+
shade = fmod(shade, hi);
399+
400+
if (shade < 0)
401+
shade += hi;
402+
403+
// And store the shade that was used to compute this new color for later use
404+
penState.shade = shade;
405+
406+
legacy_update_pen_color(penState);
407+
}

src/blocks/penblocks.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class PenBlocks : public libscratchcpp::IExtension
2323
static libscratchcpp::CompilerValue *compileSetPenColorParamTo(libscratchcpp::Compiler *compiler);
2424
static libscratchcpp::CompilerValue *compileChangePenSizeBy(libscratchcpp::Compiler *compiler);
2525
static libscratchcpp::CompilerValue *compileSetPenSizeTo(libscratchcpp::Compiler *compiler);
26+
static libscratchcpp::CompilerValue *compileChangePenShadeBy(libscratchcpp::Compiler *compiler);
2627
};
2728

2829
} // namespace scratchcpprender

test/blocks/pen_blocks_test.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2202,3 +2202,147 @@ TEST_F(PenBlocksTest, SetPenSizeTo_Stage)
22022202
thread->run();
22032203
ASSERT_EQ(model.penAttributes().diameter, 511.5);
22042204
}
2205+
2206+
TEST_F(PenBlocksTest, ChangePenShadeBy)
2207+
{
2208+
auto sprite = std::make_shared<Sprite>();
2209+
sprite->setEngine(&m_engineMock);
2210+
2211+
RenderedTarget renderedTarget;
2212+
SpriteModel model;
2213+
model.init(sprite.get());
2214+
model.setRenderedTarget(&renderedTarget);
2215+
sprite->setInterface(&model);
2216+
2217+
ScriptBuilder builder(m_extension.get(), m_engine, sprite);
2218+
builder.addBlock("pen_changePenShadeBy");
2219+
builder.addValueInput("SHADE", 49.2);
2220+
2221+
PenState &penState = model.penState();
2222+
penState.color = 60;
2223+
penState.saturation = 90;
2224+
penState.brightness = 75;
2225+
penState.shade = 13.8;
2226+
penState.updateColor();
2227+
2228+
auto original = model.penAttributes().color;
2229+
2230+
auto thread = buildScript(builder, sprite.get());
2231+
2232+
EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
2233+
thread->run();
2234+
2235+
EXPECT_EQ(model.penAttributes().color.red(), 55);
2236+
EXPECT_EQ(model.penAttributes().color.green(), 135);
2237+
EXPECT_EQ(model.penAttributes().color.blue(), 255);
2238+
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
2239+
EXPECT_EQ(model.penState().shade, 63);
2240+
}
2241+
2242+
TEST_F(PenBlocksTest, ChangePenShadeBy_OutOfRange)
2243+
{
2244+
auto sprite = std::make_shared<Sprite>();
2245+
sprite->setEngine(&m_engineMock);
2246+
2247+
RenderedTarget renderedTarget;
2248+
SpriteModel model;
2249+
model.init(sprite.get());
2250+
model.setRenderedTarget(&renderedTarget);
2251+
sprite->setInterface(&model);
2252+
2253+
ScriptBuilder builder(m_extension.get(), m_engine, sprite);
2254+
builder.addBlock("pen_changePenShadeBy");
2255+
builder.addValueInput("SHADE", 189.6);
2256+
2257+
PenState &penState = model.penState();
2258+
penState.color = 60;
2259+
penState.saturation = 90;
2260+
penState.brightness = 75;
2261+
penState.shade = 13.8;
2262+
penState.updateColor();
2263+
2264+
auto original = model.penAttributes().color;
2265+
2266+
auto thread = buildScript(builder, sprite.get());
2267+
2268+
EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
2269+
thread->run();
2270+
2271+
EXPECT_EQ(model.penAttributes().color.red(), 0);
2272+
EXPECT_EQ(model.penAttributes().color.green(), 22);
2273+
EXPECT_EQ(model.penAttributes().color.blue(), 56);
2274+
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
2275+
EXPECT_EQ(std::round(model.penState().shade * 100) / 100, 3.4);
2276+
}
2277+
2278+
TEST_F(PenBlocksTest, ChangePenShadeBy_OutOfRange_Negative)
2279+
{
2280+
auto sprite = std::make_shared<Sprite>();
2281+
sprite->setEngine(&m_engineMock);
2282+
2283+
RenderedTarget renderedTarget;
2284+
SpriteModel model;
2285+
model.init(sprite.get());
2286+
model.setRenderedTarget(&renderedTarget);
2287+
sprite->setInterface(&model);
2288+
2289+
ScriptBuilder builder(m_extension.get(), m_engine, sprite);
2290+
builder.addBlock("pen_changePenShadeBy");
2291+
builder.addValueInput("SHADE", -25.3);
2292+
2293+
PenState &penState = model.penState();
2294+
penState.color = 60;
2295+
penState.saturation = 90;
2296+
penState.brightness = 75;
2297+
penState.shade = 13.8;
2298+
penState.updateColor();
2299+
2300+
auto original = model.penAttributes().color;
2301+
2302+
auto thread = buildScript(builder, sprite.get());
2303+
2304+
EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
2305+
thread->run();
2306+
2307+
EXPECT_EQ(model.penAttributes().color.red(), 0);
2308+
EXPECT_EQ(model.penAttributes().color.green(), 36);
2309+
EXPECT_EQ(model.penAttributes().color.blue(), 91);
2310+
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
2311+
EXPECT_EQ(model.penState().shade, 188.5);
2312+
}
2313+
2314+
TEST_F(PenBlocksTest, ChangePenShadeBy_Stage)
2315+
{
2316+
auto stage = std::make_shared<Stage>();
2317+
stage->setEngine(&m_engineMock);
2318+
2319+
RenderedTarget renderedTarget;
2320+
StageModel model;
2321+
model.init(stage.get());
2322+
model.setRenderedTarget(&renderedTarget);
2323+
stage->setInterface(&model);
2324+
2325+
ScriptBuilder builder(m_extension.get(), m_engine, stage);
2326+
builder.addBlock("pen_changePenShadeBy");
2327+
builder.addValueInput("SHADE", 49.2);
2328+
2329+
PenState &penState = model.penState();
2330+
penState.color = 60;
2331+
penState.saturation = 90;
2332+
penState.brightness = 75;
2333+
penState.shade = 13.8;
2334+
penState.updateColor();
2335+
2336+
auto original = model.penAttributes().color;
2337+
2338+
auto thread = buildScript(builder, stage.get());
2339+
2340+
EXPECT_CALL(m_engineMock, requestRedraw).Times(0);
2341+
thread->run();
2342+
2343+
EXPECT_EQ(model.penAttributes().color.red(), 55);
2344+
EXPECT_EQ(model.penAttributes().color.green(), 135);
2345+
EXPECT_EQ(model.penAttributes().color.blue(), 255);
2346+
EXPECT_EQ(model.penAttributes().color.alpha(), 255);
2347+
EXPECT_EQ(model.penState().shade, 63);
2348+
}

0 commit comments

Comments
 (0)