Something went wrong. Try again.
google balls
Something went wrong. Try again.
16 kB · 576 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577#include <AppFileInfo.h>#include <Application.h>#include <Bitmap.h>#include <File.h>#include <FindDirectory.h>#include <Menu.h>#include <MenuBar.h>#include <MenuItem.h>#include <OS.h>#include <Path.h>#include <Resources.h>#include <Roster.h>#include <Screen.h>#include <TranslationUtils.h>#include <View.h>#include <Window.h>#include <cmath>#include <cstring>#include <string>#include <vector>
const uint32 MSG_PULSE = 'puls';const uint32 MSG_TOGGLE_VSYNC = 'vsyn';const uint32 MSG_TOGGLE_UNCAPPED = 'uncap';const uint32 MSG_TOGGLE_FPS_COUNTER = 'tfps';const uint32 MSG_TOGGLE_DARK_MODE = 'dark';
struct Vector3 { double x, y, z;
Vector3(double x = 0, double y = 0, double z = 0) : x(x), y(y), z(z) {}
void set(double newX, double newY, double newZ = 0) { x = newX; y = newY; z = newZ; }};
struct Color { uint8 r, g, b, a;
Color(uint8 r = 255, uint8 g = 255, uint8 b = 255, uint8 a = 255) : r(r), g(g), b(b), a(a) {}
static Color fromHex(const std::string &hex) { if (hex[0] == '#') { unsigned int value = std::stoul(hex.substr(1), nullptr, 16); return Color((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF, 255); } return Color(); }
rgb_color toRGBColor() const { rgb_color c; c.red = r; c.green = g; c.blue = b; c.alpha = a; return c; }};
class Point {public: Vector3 curPos, originalPos, targetPos, velocity; Color color; double radius, size; double friction = 0.8; double springStrength = 0.1;
Point(double x, double y, double z, double size, const std::string &colorHex) : curPos(x, y, z), originalPos(x, y, z), targetPos(x, y, z), velocity(0, 0, 0), size(size), radius(size) { color = Color::fromHex(colorHex); }
void update(double dt) { // Time scaling factor (target 30fps = ~0.033s) double timeScale = dt / 0.033;
// X axis spring physics double dx = targetPos.x - curPos.x; double ax = dx * springStrength * timeScale; velocity.x += ax; velocity.x *= std::pow(friction, timeScale);
if (std::abs(dx) < 0.1 && std::abs(velocity.x) < 0.01) { curPos.x = targetPos.x; velocity.x = 0; } else { curPos.x += velocity.x * timeScale; }
// Y axis spring physics double dy = targetPos.y - curPos.y; double ay = dy * springStrength * timeScale; velocity.y += ay; velocity.y *= std::pow(friction, timeScale);
if (std::abs(dy) < 0.1 && std::abs(velocity.y) < 0.01) { curPos.y = targetPos.y; velocity.y = 0; } else { curPos.y += velocity.y * timeScale; }
// Z axis (depth) double dox = originalPos.x - curPos.x; double doy = originalPos.y - curPos.y; double dd = (dox * dox) + (doy * doy); double d = std::sqrt(dd);
targetPos.z = d / 100.0 + 1.0; double dz = targetPos.z - curPos.z; double az = dz * springStrength * timeScale; velocity.z += az; velocity.z *= std::pow(friction, timeScale);
if (std::abs(dz) < 0.01 && std::abs(velocity.z) < 0.001) { curPos.z = targetPos.z; velocity.z = 0; } else { curPos.z += velocity.z * timeScale; }
radius = size * curPos.z; if (radius < 1) radius = 1; }
void draw(BView *view) { rgb_color c = color.toRGBColor(); view->SetHighColor(c); view->SetPenSize(0); // Fill doesn't use pen size, but good practice
// Use native FillEllipse for better performance // Haiku coordinates are center of pixel, so simple arithmetic works BRect rect(curPos.x - radius, curPos.y - radius, curPos.x + radius, curPos.y + radius); view->FillEllipse(rect); }};
struct PointData { int x, y; int size; std::string color;};
static void computeBounds(const std::vector<PointData> &data, double &w, double &h) { int minX = 99999, maxX = -99999; int minY = 99999, maxY = -99999;
for (const auto &p : data) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; }
w = maxX - minX; h = maxY - minY;}
class PointCollection {public: Vector3 mousePos; std::vector<Point> points;
PointCollection() : mousePos(0, 0, 0) {}
void addPoint(double x, double y, double z, double size, const std::string &color) { points.emplace_back(x, y, z, size, color); }
void update(double dt) { for (auto &point : points) { double dx = mousePos.x - point.curPos.x; double dy = mousePos.y - point.curPos.y; double dd = (dx * dx) + (dy * dy); double d = std::sqrt(dd);
if (d < 150) { point.targetPos.x = point.curPos.x - dx; point.targetPos.y = point.curPos.y - dy; } else { point.targetPos.x = point.originalPos.x; point.targetPos.y = point.originalPos.y; }
point.update(dt); } }
void draw(BView *view) { for (auto &point : points) { point.draw(view); } }};
class BallsView : public BView {public: BallsView(BRect frame) : BView(frame, "BallsView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS | B_PULSE_NEEDED), fOffscreenBitmap(nullptr), fOffscreenView(nullptr), fLastTime(0), fFrameCount(0), fFpsTime(0), fCurrentFps(0.0), fShowFps(false), fDarkMode(false) { SetViewColor(B_TRANSPARENT_COLOR); initPoints(); }
~BallsView() { delete fOffscreenBitmap; }
virtual void AttachedToWindow() { BView::AttachedToWindow(); SetEventMask(B_POINTER_EVENTS, B_NO_POINTER_HISTORY); fLastTime = system_time(); _InitDoubleBuffering(); }
virtual void Draw(BRect updateRect) { if (!fOffscreenBitmap || !fOffscreenBitmap->IsValid()) return;
if (fOffscreenBitmap->Lock()) { if (fDarkMode) { fOffscreenView->SetHighColor(26, 26, 26); } else { fOffscreenView->SetHighColor(255, 255, 255); } fOffscreenView->FillRect(fOffscreenView->Bounds());
fOffscreenView->SetDrawingMode(B_OP_ALPHA); pointCollection.draw(fOffscreenView);
if (fShowFps) { char fpsText[32]; snprintf(fpsText, sizeof(fpsText), "FPS: %.1f", fCurrentFps); if (fDarkMode) { fOffscreenView->SetHighColor(255, 255, 255); } else { fOffscreenView->SetHighColor(0, 0, 0); } fOffscreenView->SetDrawingMode(B_OP_OVER); fOffscreenView->DrawString(fpsText, BPoint(10, 20)); }
fOffscreenView->Sync(); fOffscreenBitmap->Unlock(); }
DrawBitmap(fOffscreenBitmap, BPoint(0, 0)); }
virtual void MouseMoved(BPoint where, uint32 code, const BMessage *dragMessage) { pointCollection.mousePos.set(where.x, where.y); }
virtual void FrameResized(float width, float height) { BView::FrameResized(width, height); recenterPoints(width, height); _InitDoubleBuffering(); }
virtual void Pulse() { bigtime_t now = system_time(); double dt = (now - fLastTime) / 1000000.0; fLastTime = now;
if (dt > 0.1) dt = 0.1;
fFrameCount++; if (now - fFpsTime >= 1000000) { fCurrentFps = fFrameCount * 1000000.0 / (now - fFpsTime); fFrameCount = 0; fFpsTime = now; }
pointCollection.update(dt); Invalidate(); }
void SetShowFps(bool show) { fShowFps = show; Invalidate(); }
void SetDarkMode(bool dark) { fDarkMode = dark; Invalidate(); }
bool IsDarkMode() const { return fDarkMode; }
private: PointCollection pointCollection; BBitmap *fOffscreenBitmap; BView *fOffscreenView; bigtime_t fLastTime; int fFrameCount; bigtime_t fFpsTime; double fCurrentFps; bool fShowFps; bool fDarkMode;
void _InitDoubleBuffering() { delete fOffscreenBitmap; fOffscreenBitmap = new BBitmap(Bounds(), B_RGBA32, true); if (fOffscreenBitmap->IsValid()) { fOffscreenView = new BView(Bounds(), "offscreen", B_FOLLOW_ALL, B_WILL_DRAW); fOffscreenBitmap->AddChild(fOffscreenView); } else { delete fOffscreenBitmap; fOffscreenBitmap = nullptr; fOffscreenView = nullptr; } }
void initPoints() { std::vector<PointData> pointData = { {202, 78, 9, "#ed9d33"}, {348, 83, 9, "#d44d61"}, {256, 69, 9, "#4f7af2"}, {214, 59, 9, "#ef9a1e"}, {265, 36, 9, "#4976f3"}, {300, 78, 9, "#269230"}, {294, 59, 9, "#1f9e2c"}, {45, 88, 9, "#1c48dd"}, {268, 52, 9, "#2a56ea"}, {73, 83, 9, "#3355d8"}, {294, 6, 9, "#36b641"}, {235, 62, 9, "#2e5def"}, {353, 42, 8, "#d53747"}, {336, 52, 8, "#eb676f"}, {208, 41, 8, "#f9b125"}, {321, 70, 8, "#de3646"}, {8, 60, 8, "#2a59f0"}, {180, 81, 8, "#eb9c31"}, {146, 65, 8, "#c41731"}, {145, 49, 8, "#d82038"}, {246, 34, 8, "#5f8af8"}, {169, 69, 8, "#efa11e"}, {273, 99, 8, "#2e55e2"}, {248, 120, 8, "#4167e4"}, {294, 41, 8, "#0b991a"}, {267, 114, 8, "#4869e3"}, {78, 67, 8, "#3059e3"}, {294, 23, 8, "#10a11d"}, {117, 83, 8, "#cf4055"}, {137, 80, 8, "#cd4359"}, {14, 71, 8, "#2855ea"}, {331, 80, 8, "#ca273c"}, {25, 82, 8, "#2650e1"}, {233, 46, 8, "#4a7bf9"}, {73, 13, 8, "#3d65e7"}, {327, 35, 6, "#f47875"}, {319, 46, 6, "#f36764"}, {256, 81, 6, "#1d4eeb"}, {244, 88, 6, "#698bf1"}, {194, 32, 6, "#fac652"}, {97, 56, 6, "#ee5257"}, {105, 75, 6, "#cf2a3f"}, {42, 4, 6, "#5681f5"}, {10, 27, 6, "#4577f6"}, {166, 55, 6, "#f7b326"}, {266, 88, 6, "#2b58e8"}, {178, 34, 6, "#facb5e"}, {100, 65, 6, "#e02e3d"}, {343, 32, 6, "#f16d6f"}, {59, 5, 6, "#507bf2"}, {27, 9, 6, "#5683f7"}, {233, 116, 6, "#3158e2"}, {123, 32, 6, "#f0696c"}, {6, 38, 6, "#3769f6"}, {63, 62, 6, "#6084ef"}, {6, 49, 6, "#2a5cf4"}, {108, 36, 6, "#f4716e"}, {169, 43, 6, "#f8c247"}, {137, 37, 6, "#e74653"}, {318, 58, 6, "#ec4147"}, {226, 100, 5, "#4876f1"}, {101, 46, 5, "#ef5c5c"}, {226, 108, 5, "#2552ea"}, {17, 17, 5, "#4779f7"}, {232, 93, 5, "#4b78f1"}};
BRect bounds = Bounds(); double logoW, logoH; computeBounds(pointData, logoW, logoH);
double offsetX = (bounds.Width() / 2.0) - (logoW / 2.0); double offsetY = (bounds.Height() / 2.0) - (logoH / 2.0);
for (const auto &data : pointData) { double x = offsetX + data.x; double y = offsetY + data.y; pointCollection.addPoint(x, y, 0.0, static_cast<double>(data.size), data.color); } }
void recenterPoints(float width, float height) { // Offset delta calculation double oldOffX = (fOffscreenBitmap ? fOffscreenBitmap->Bounds().Width() / 2.0 : width / 2.0) - 180; double oldOffY = (fOffscreenBitmap ? fOffscreenBitmap->Bounds().Height() / 2.0 : height / 2.0) - 65; double newOffX = (width / 2.0) - 180; double newOffY = (height / 2.0) - 65; double diffX = newOffX - oldOffX; double diffY = newOffY - oldOffY;
for (auto &point : pointCollection.points) { point.originalPos.x += diffX; point.originalPos.y += diffY; point.curPos.x = point.originalPos.x; point.curPos.y = point.originalPos.y; } }};
class BallsWindow : public BWindow {public: BallsWindow() : BWindow(BRect(100, 100, 900, 700), "Google Balls", B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE), fUncapped(false) {
BRect bounds = Bounds();
// Create menu bar fMenuBar = new BMenuBar(BRect(0, 0, bounds.Width(), 18), "menubar"); AddChild(fMenuBar);
BMenu *fpsMenu = new BMenu("FPS"); fVSyncItem = new BMenuItem("VSync (60 FPS)", new BMessage(MSG_TOGGLE_VSYNC)); fpsMenu->AddItem(fVSyncItem);
fUncappedItem = new BMenuItem("Uncapped", new BMessage(MSG_TOGGLE_UNCAPPED)); fpsMenu->AddItem(fUncappedItem);
fpsMenu->AddSeparatorItem();
fShowFpsItem = new BMenuItem("Show FPS", new BMessage(MSG_TOGGLE_FPS_COUNTER)); fpsMenu->AddItem(fShowFpsItem);
fMenuBar->AddItem(fpsMenu);
BMenu *viewMenu = new BMenu("View"); fDarkModeItem = new BMenuItem("Dark Mode", new BMessage(MSG_TOGGLE_DARK_MODE), 'D'); viewMenu->AddItem(fDarkModeItem);
fMenuBar->AddItem(viewMenu);
// Initial state (will be overridden by LoadSettings) fVSyncItem->SetMarked(true); fUncappedItem->SetMarked(false); fShowFpsItem->SetMarked(false); fDarkModeItem->SetMarked(false);
// Adjust view to account for menu bar bounds.top = fMenuBar->Frame().bottom + 1; fView = new BallsView(bounds); AddChild(fView);
LoadSettings();
if (fUncapped) SetPulseRate(1000); else SetPulseRate(16000); }
virtual ~BallsWindow() { SaveSettings(); }
virtual void MessageReceived(BMessage *message) { switch (message->what) { case MSG_TOGGLE_VSYNC: fUncapped = false; fVSyncItem->SetMarked(true); fUncappedItem->SetMarked(false); SetPulseRate(16000); break; case MSG_TOGGLE_UNCAPPED: fUncapped = true; fVSyncItem->SetMarked(false); fUncappedItem->SetMarked(true); SetPulseRate(1000); break; case MSG_TOGGLE_FPS_COUNTER: fShowFpsItem->SetMarked(!fShowFpsItem->IsMarked()); fView->SetShowFps(fShowFpsItem->IsMarked()); break; case MSG_TOGGLE_DARK_MODE: fDarkModeItem->SetMarked(!fDarkModeItem->IsMarked()); fView->SetDarkMode(fDarkModeItem->IsMarked()); SaveSettings(); break; default: BWindow::MessageReceived(message); break; } }
virtual bool QuitRequested() { be_app->PostMessage(B_QUIT_REQUESTED); return true; }
private: void SaveSettings() { BPath path; if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) return; path.Append("GoogleBalls_settings");
BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE); if (file.InitCheck() != B_OK) return;
BMessage settings; settings.AddBool("uncapped", fUncapped); settings.AddBool("showfps", fShowFpsItem->IsMarked()); settings.AddBool("darkmode", fDarkModeItem->IsMarked()); settings.Flatten(&file); }
void LoadSettings() { BPath path; if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) return; path.Append("GoogleBalls_settings");
BFile file(path.Path(), B_READ_ONLY); if (file.InitCheck() != B_OK) return;
BMessage settings; if (settings.Unflatten(&file) != B_OK) return;
bool uncapped = false; if (settings.FindBool("uncapped", &uncapped) == B_OK) { fUncapped = uncapped; fVSyncItem->SetMarked(!uncapped); fUncappedItem->SetMarked(uncapped); }
bool showfps = false; if (settings.FindBool("showfps", &showfps) == B_OK) { fShowFpsItem->SetMarked(showfps); fView->SetShowFps(showfps); }
bool darkmode = false; if (settings.FindBool("darkmode", &darkmode) == B_OK) { fDarkModeItem->SetMarked(darkmode); fView->SetDarkMode(darkmode); } }
BallsView *fView; BMenuBar *fMenuBar; BMenuItem *fVSyncItem; BMenuItem *fUncappedItem; BMenuItem *fShowFpsItem; BMenuItem *fDarkModeItem; bool fUncapped;};
class BallsApp : public BApplication {public: BallsApp() : BApplication("application/x-vnd.GoogleBalls") { fWindow = new BallsWindow(); fWindow->Show(); }
private: BallsWindow *fWindow;};
int main() { BallsApp app; app.Run(); return 0;}