Skip to content

Implement text bubbles #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ qt_add_qml_module(scratchcpp-render
internal/ValueMonitor.qml
internal/MonitorSlider.qml
internal/ListMonitor.qml
internal/TextBubble.qml
shaders/sprite.vert
shaders/sprite.frag
SOURCES
Expand Down Expand Up @@ -62,6 +63,10 @@ qt_add_qml_module(scratchcpp-render
shadermanager.h
graphicseffect.cpp
graphicseffect.h
textbubbleshape.cpp
textbubbleshape.h
textbubblepainter.cpp
textbubblepainter.h
blocks/penextension.cpp
blocks/penextension.h
blocks/penblocks.cpp
Expand Down
52 changes: 42 additions & 10 deletions src/ProjectPlayer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ ProjectScene {
onStageModelChanged: stageModel.renderedTarget = this
}

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
}
}

PenLayer {
id: projectPenLayer
engine: loader.engine
Expand All @@ -118,16 +132,34 @@ ProjectScene {
Component {
id: renderedSprite

RenderedTarget {
id: target
mouseArea: sceneMouseArea
stageScale: root.stageScale
transform: Scale { xScale: mirrorHorizontally ? -1 : 1 }
Component.onCompleted: {
engine = loader.engine;
spriteModel = modelData;
spriteModel.renderedTarget = this;
spriteModel.penLayer = projectPenLayer;
Item {
anchors.fill: parent

RenderedTarget {
id: targetItem
mouseArea: sceneMouseArea
stageScale: root.stageScale
transform: Scale { xScale: targetItem.mirrorHorizontally ? -1 : 1 }
Component.onCompleted: {
engine = loader.engine;
spriteModel = modelData;
spriteModel.renderedTarget = this;
spriteModel.penLayer = projectPenLayer;
}
}

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
}
}
}
}
Expand Down
106 changes: 106 additions & 0 deletions src/internal/TextBubble.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: LGPL-3.0-or-later

import QtQuick
import ScratchCPP.Render

TextBubbleShape {
id: root
property string text: ""
property RenderedTarget target: null
property double stageWidth: 0
property double stageHeight: 0

QtObject {
// https://github.com/scratchfoundation/scratch-render/blob/ac935423afe3ba79235750eecb1e443474c6eb09/src/TextBubbleSkin.js#L7-L26
id: priv
readonly property int maxLineWidth: 170
readonly property int minWidth: 50
readonly property int padding: 10
readonly property int tailHeight: 12

readonly property string fontFamily: "Helvetica"
readonly property int fontPixelSize: 14
readonly property int lineHeight: 16

readonly property color textFill: '#575E75'

function translateX(x) {
// Translates Scratch X-coordinate to the scene coordinate system
return root.stageScale * (root.stageWidth / 2 + x)
}

function translateY(y) {
// Translates Scratch Y-coordinate to the scene coordinate system
return root.stageScale * (root.stageHeight / 2 - y)
}
}

nativeWidth: Math.max(bubbleText.contentWidth, priv.minWidth) + 2 * priv.padding
nativeHeight: bubbleText.height + 2 * priv.padding + priv.tailHeight

function positionBubble() {
// https://github.com/scratchfoundation/scratch-vm/blob/7313ce5199f8a3da7850085d0f7f6a3ca2c89bf6/src/blocks/scratch3_looks.js#L158
if(!target.visible)
return;

const targetBounds = target.getBoundsForBubble();
const stageBounds = Qt.rect(-root.stageWidth / 2, root.stageHeight / 2, root.stageWidth, root.stageHeight);

if (onSpriteRight && nativeWidth + targetBounds.right > stageBounds.right &&
(targetBounds.left - nativeWidth > stageBounds.left)) { // Only flip if it would fit
onSpriteRight = false;
} else if (!onSpriteRight && targetBounds.left - nativeWidth < stageBounds.left &&
(nativeWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
onSpriteRight = true;
}

const pos = [
onSpriteRight ? (
Math.max(
stageBounds.left, // Bubble should not extend past left edge of stage
Math.min(stageBounds.right - nativeWidth, targetBounds.right)
)
) : (
Math.min(
stageBounds.right - nativeWidth, // Bubble should not extend past right edge of stage
Math.max(stageBounds.left, targetBounds.left - nativeWidth)
)
),
// Bubble should not extend past the top of the stage
Math.min(stageBounds.top, targetBounds.bottom + nativeHeight)
];

x = priv.translateX(pos[0]);
y = priv.translateY(pos[1]);
}

Connections {
target: root.target

function onXChanged() { positionBubble() }
function onYChanged() { positionBubble() }
function onRotationChanged() { positionBubble() }
function onWidthChanged() { positionBubble() }
function onHeightChanged() { positionBubble() }
function onScaleChanged() { positionBubble() }
}

Text {
id: bubbleText
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: priv.padding * root.stageScale
width: priv.maxLineWidth
scale: root.stageScale
transformOrigin: Item.TopLeft
text: root.text
wrapMode: Text.Wrap
color: priv.textFill
lineHeight: priv.lineHeight
lineHeightMode: Text.FixedHeight
font.family: priv.fontFamily
font.pixelSize: priv.fontPixelSize
}

Component.onCompleted: positionBubble()
}
1 change: 1 addition & 0 deletions src/irenderedtarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class IRenderedTarget : public QNanoQuickItem
virtual void setHeight(qreal width) = 0;

virtual libscratchcpp::Rect getBounds() const = 0;
virtual QRectF getBoundsForBubble() const = 0;

virtual QPointF mapFromScene(const QPointF &point) const = 0;

Expand Down
16 changes: 13 additions & 3 deletions src/renderedtarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,19 @@ Rect RenderedTarget::getBounds() const
return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y);
}

QRectF RenderedTarget::getBoundsForBubble() const
{
// https://github.com/scratchfoundation/scratch-render/blob/86dcb0151a04bc8c1ff39559e8531e7921102b56/src/Drawable.js#L536-L551
Rect rect = getBounds();
const int slice = 8; // px, how tall the top slice to measure should be

if (rect.height() > slice)
rect.setBottom(rect.top() - slice);

Q_ASSERT(rect.height() <= 8);
return QRectF(QPointF(rect.left(), rect.top()), QPointF(rect.right(), rect.bottom()));
}

QPointF RenderedTarget::mapFromScene(const QPointF &point) const
{
return QNanoQuickItem::mapFromScene(point);
Expand Down Expand Up @@ -463,9 +476,6 @@ void RenderedTarget::clearGraphicEffects()

void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo)
{
if (m_stageModel)
return; // hull points are useless for the stage

Q_ASSERT(fbo);
int width = fbo->width();
int height = fbo->height();
Expand Down
1 change: 1 addition & 0 deletions src/renderedtarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class RenderedTarget : public IRenderedTarget
void setHeight(qreal height) override;

libscratchcpp::Rect getBounds() const override;
Q_INVOKABLE QRectF getBoundsForBubble() const override;

QPointF mapFromScene(const QPointF &point) const override;

Expand Down
37 changes: 37 additions & 0 deletions src/spritemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ void SpriteModel::onGraphicsEffectsCleared()
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();
}
}

libscratchcpp::Rect SpriteModel::boundingRect() const
{
return m_renderedTarget->getBounds();
Expand Down Expand Up @@ -187,4 +214,14 @@ SpriteModel *SpriteModel::cloneRoot() const
return m_cloneRoot;
}

const TextBubbleShape::Type &SpriteModel::bubbleType() const
{
return m_bubbleType;
}

const QString &SpriteModel::bubbleText() const
{
return m_bubbleText;
}

} // namespace scratchcpprender
14 changes: 14 additions & 0 deletions src/spritemodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <scratchcpp/ispritehandler.h>

#include "penstate.h"
#include "textbubbleshape.h"

Q_MOC_INCLUDE("renderedtarget.h");
Q_MOC_INCLUDE("ipenlayer.h");
Expand All @@ -25,6 +26,8 @@ class SpriteModel
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);
Expand All @@ -48,6 +51,9 @@ 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;

libscratchcpp::Rect boundingRect() const override;

libscratchcpp::Sprite *sprite() const;
Expand All @@ -66,9 +72,15 @@ class SpriteModel

SpriteModel *cloneRoot() const;

const TextBubbleShape::Type &bubbleType() const;

const QString &bubbleText() const;

signals:
void renderedTargetChanged();
void penLayerChanged();
void bubbleTypeChanged();
void bubbleTextChanged();
void cloned(SpriteModel *cloneModel);
void cloneDeleted(SpriteModel *clone);

Expand All @@ -78,6 +90,8 @@ class SpriteModel
IPenLayer *m_penLayer = nullptr;
PenState m_penState;
SpriteModel *m_cloneRoot = nullptr;
TextBubbleShape::Type m_bubbleType = TextBubbleShape::Type::Say;
QString m_bubbleText;
};

} // namespace scratchcpprender
37 changes: 37 additions & 0 deletions src/stagemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ void StageModel::onGraphicsEffectsCleared()
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();
}
}

void StageModel::loadCostume()
{
if (m_renderedTarget && m_stage) {
Expand Down Expand Up @@ -78,3 +105,13 @@ void StageModel::setRenderedTarget(IRenderedTarget *newRenderedTarget)

emit renderedTargetChanged();
}

const TextBubbleShape::Type &StageModel::bubbleType() const
{
return m_bubbleType;
}

const QString &StageModel::bubbleText() const
{
return m_bubbleText;
}
Loading