Last active
January 23, 2022 15:46
-
-
Save sthairno/fb460eb9ab3f4af9921abe5727b46f15 to your computer and use it in GitHub Desktop.
単語の区切りを考慮した折返し機能付きテキストレンダラー
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# include <Siv3D.hpp> // OpenSiv3D v0.6.3 | |
class TextRenderer | |
{ | |
public: | |
TextRenderer(Font& font) | |
:m_font(font) | |
{ | |
} | |
public: | |
static constexpr std::array<char32, 2> newLineChars{ U'\r', U'\n' }; | |
void setText(const String& text) | |
{ | |
m_text = text; | |
m_glyphs = m_font.getGlyphs(text); | |
} | |
SizeF setWidth(double width) | |
{ | |
m_newLinePositions.clear(); | |
m_lineWidthList.clear(); | |
m_lineWidthList.push_back(0); | |
std::deque<Group> groups; | |
// グループの作成 | |
{ | |
bool inGroup = false; | |
for (auto [idx, glyph] : Indexed(m_glyphs)) | |
{ | |
bool groupChar = false | |
|| (U'a' <= glyph.codePoint && glyph.codePoint <= U'z') | |
|| (U'A' <= glyph.codePoint && glyph.codePoint <= U'Z') | |
|| glyph.codePoint == U'.' | |
|| glyph.codePoint == U',' | |
|| glyph.codePoint == U'\''; | |
if (groupChar != inGroup) | |
{ | |
if (groupChar) | |
{ | |
groups.emplace_back(Group{ | |
.begin = idx, | |
.count = 0, | |
.xAdvance = 0 | |
}); | |
} | |
inGroup = groupChar; | |
} | |
if (inGroup) | |
{ | |
Group& group = groups.back(); | |
group.count++; | |
group.width = group.xAdvance + glyph.left + glyph.width; | |
group.xAdvance += glyph.xAdvance; | |
} | |
} | |
} | |
// 改行位置,サイズを計算 | |
{ | |
m_renderSize.x = 0; | |
double nextX = 0; | |
for (size_t idx = 0; idx < m_glyphs.size();) | |
{ | |
Glyph& glyph = m_glyphs[idx]; | |
// グループ | |
if (not groups.empty() && idx == groups.front().begin) | |
{ | |
Group& group = groups.front(); | |
if (nextX + group.width > width) | |
{ | |
nextX = 0; | |
if (idx != 0) | |
{ | |
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx - 1) | |
{ | |
m_newLinePositions.push_back(idx - 1); | |
m_lineWidthList.push_back(0); | |
} | |
} | |
} | |
groups.pop_front(); | |
} | |
if (std::find(newLineChars.cbegin(), newLineChars.cend(), glyph.codePoint) != newLineChars.cend()) | |
{ | |
// 改行文字 | |
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx) | |
{ | |
m_newLinePositions.push_back(idx); | |
m_lineWidthList.push_back(0); | |
} | |
nextX = 0; | |
idx += 1; | |
} | |
else | |
{ | |
// その他の文字 | |
double renderWidth = nextX + glyph.left + glyph.width; | |
if (renderWidth > width) | |
{ | |
nextX = 0; | |
renderWidth = nextX + glyph.left + glyph.width; | |
if (idx != 0) | |
{ | |
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx - 1) | |
{ | |
m_newLinePositions.push_back(idx - 1); | |
m_lineWidthList.push_back(0); | |
} | |
} | |
} | |
m_lineWidthList.back() = renderWidth; | |
m_renderSize.x = Max(m_renderSize.x, renderWidth); | |
nextX += glyph.xAdvance; | |
idx += 1; | |
} | |
} | |
m_renderSize.y = m_glyphs.empty() | |
? 0.0 | |
: m_font.height() * (m_newLinePositions.size() + 1); | |
} | |
return m_renderSize; | |
} | |
RectF draw(Vec2 pos, ColorF color) | |
{ | |
size_t lineIdx = 0; | |
size_t beginIdx = 0; | |
size_t endIdx; | |
auto nextNewLinePos = m_newLinePositions.cbegin(); | |
double lineY = pos.y + m_font.ascender(); | |
while (nextNewLinePos != m_newLinePositions.cend()) | |
{ | |
endIdx = *nextNewLinePos; | |
drawLine( | |
beginIdx, | |
endIdx, | |
{ | |
pos.x + (m_renderSize.x - m_lineWidthList[lineIdx++]) / 2, | |
lineY | |
}, | |
color | |
); | |
lineY += m_font.height(); | |
beginIdx = endIdx + 1; | |
nextNewLinePos++; | |
} | |
endIdx = m_glyphs.size() - 1; | |
if (beginIdx <= endIdx) | |
{ | |
drawLine( | |
beginIdx, | |
endIdx, | |
{ | |
pos.x + (m_renderSize.x - m_lineWidthList[lineIdx]) / 2, | |
lineY | |
}, | |
color | |
); | |
} | |
return { pos, m_renderSize }; | |
} | |
private: | |
struct Group | |
{ | |
size_t begin; | |
int64_t count; | |
double width; | |
double xAdvance; | |
}; | |
Font& m_font; | |
String m_text; | |
SizeF m_renderSize; | |
Array<Glyph> m_glyphs; | |
Array<size_t> m_newLinePositions; | |
Array<double> m_lineWidthList; | |
void drawLine(size_t beginIdx, size_t endIdx, Vec2 pos, ColorF color) | |
{ | |
// 文字の描画 | |
for (size_t idx : Range(beginIdx, endIdx)) | |
{ | |
Glyph& glyph = m_glyphs[idx]; | |
if (std::find(newLineChars.cbegin(), newLineChars.cend(), glyph.codePoint) == newLineChars.cend()) | |
{ | |
glyph.texture | |
.draw(pos + Vec2(glyph.left, -glyph.top), color); | |
pos.x += glyph.xAdvance; | |
} | |
} | |
} | |
}; | |
void Main() | |
{ | |
Window::SetStyle(WindowStyle::Sizable); | |
Scene::SetBackground(ColorF(0.6)); | |
constexpr StringView targetFileName = U"text.txt"; | |
const String targetFileDir = FileSystem::CurrentDirectory(); | |
const String targetFilePath = Format(targetFileDir, targetFileName); | |
Font font(20); | |
Stopwatch stw; | |
MillisecondsF setTextTime; | |
MillisecondsF setWidthTime; | |
MillisecondsF drawTime; | |
TextRenderer renderer(font); | |
{ | |
String text; | |
if (FileSystem::Exists(targetFilePath)) | |
{ | |
text = TextReader(targetFilePath).readAll(); | |
} | |
else | |
{ | |
text = TextReader(U"example/txt/en.txt").readAll(); | |
TextWriter(targetFilePath).write(text); | |
} | |
stw.restart(); | |
renderer.setText(text); | |
setTextTime = stw.elapsed(); | |
} | |
DirectoryWatcher watcher(targetFileDir); | |
Size sceneSize = Scene::Size(); | |
RectF drawArea = { | |
20, | |
40, | |
sceneSize.x - 40, | |
sceneSize.y - 60 | |
}; | |
bool dragging = false; | |
bool paramChanged = true; | |
double textHeight = 0.0; | |
Vec2 start = { 10, 20 }; | |
Vec2 end = { 20, 4 }; | |
Vec2 diff = end - start; | |
while (System::Update()) | |
{ | |
// ファイルの更新を監視 | |
{ | |
Array<FileChange> changes; | |
if (watcher.retrieveChanges(changes) && | |
changes.includes_if([&](const FileChange& change) | |
{ | |
switch (change.action) | |
{ | |
case FileAction::Added: | |
case FileAction::Modified: | |
return change.path == targetFilePath; | |
default: | |
return false; | |
} | |
}) | |
) | |
{ | |
String text = TextReader(targetFilePath).readAll(); | |
stw.restart(); | |
renderer.setText(text); | |
setTextTime = stw.elapsed(); | |
paramChanged = true; | |
} | |
} | |
// drawAreaのサイズ変更 | |
if (dragging) | |
{ | |
if (MouseL.pressed()) | |
{ | |
Cursor::RequestStyle(CursorStyle::ResizeNWSE); | |
auto delta = Cursor::DeltaF(); | |
if (delta != Vec2::Zero()) | |
{ | |
drawArea.size += delta; | |
drawArea.w = Max(drawArea.w, 2.0); | |
drawArea.h = Max(drawArea.h, 2.0); | |
paramChanged = true; | |
} | |
} | |
else | |
{ | |
dragging = false; | |
} | |
} | |
// Scene::Size()の監視 | |
if (not dragging) | |
{ | |
auto newSceneSize = Scene::Size(); | |
if (newSceneSize != sceneSize) | |
{ | |
sceneSize = newSceneSize; | |
drawArea = { | |
20, | |
40, | |
sceneSize.x - 40, | |
sceneSize.y - 60 | |
}; | |
paramChanged = true; | |
} | |
} | |
// 描画 | |
drawArea | |
.draw(Palette::Whitesmoke) | |
.drawFrame(0, 4, Palette::Gray); | |
Circle grip(drawArea.br(), 10); | |
if (grip.mouseOver()) | |
{ | |
Cursor::RequestStyle(CursorStyle::ResizeNWSE); | |
if (MouseL.down()) | |
{ | |
dragging = true; | |
} | |
} | |
// TextRendererの更新,描画 | |
if (paramChanged) | |
{ | |
{ | |
stw.restart(); | |
renderer.setWidth(drawArea.w); | |
setWidthTime = stw.elapsed(); | |
} | |
paramChanged = true; | |
} | |
RectF textRect; | |
{ | |
stw.restart(); | |
textRect = renderer.draw(drawArea.pos, Palette::Black); | |
drawTime = stw.elapsed(); | |
} | |
textRect.drawFrame(1, 0, Palette::Red); | |
ClearPrint(); | |
Print.write(U"SetText: ", setTextTime); | |
Print.write(U", "); | |
Print.write(U"SetWidth: ", setWidthTime); | |
Print.write(U", "); | |
Print.write(U"Draw: ", drawTime); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment