From 30724a4a77341e87b3d760ac448af6785d82763b Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Thu, 17 Sep 2026 20:22:57 -0700 Subject: [PATCH 1/2] feat: port to The Switch --- .github/workflows/build.yml | 23 +++ README.md | 3 +- docs/compiling.md | 14 ++ docs/installing.md | 4 + switch/.gitignore | 5 + switch/Makefile | 140 +++++++++++++ switch/docker.mk | 24 +++ switch/icon.jpg | Bin 0 -> 10770 bytes switch/source/main.cpp | 402 ++++++++++++++++++++++++++++++++++++ 9 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 switch/.gitignore create mode 100644 switch/Makefile create mode 100644 switch/docker.mk create mode 100644 switch/icon.jpg create mode 100644 switch/source/main.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2a1fae..381ed66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -771,6 +771,29 @@ jobs: path: | ./3ds/*.3dsx ./3ds/*.smdh + build-switch: + runs-on: ubuntu-latest + container: devkitpro/devkita64:20260219 + defaults: + run: + working-directory: ./switch + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Switch application + run: make -j$(nproc) + + - name: Verify .nro exists + run: test -s google-balls.nro + + - name: Upload Switch artifact + uses: actions/upload-artifact@v4 + with: + name: google-balls-switch + path: switch/google-balls.nro + if-no-files-found: error build-ds: runs-on: ubuntu-latest container: skylyrac/blocksds:dev-latest diff --git a/README.md b/README.md index ded1fd5..f5dd9ed 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Google for making the original doodle back in 2010! [Rob Hawkes](https://github.com/robhawkes) for making the remake of the doodle that this project uses as a base! -# Available in *24* flavors! +# Available in *25* flavors! We have: - Electron (the og but bulky) - Windows, Linux, macOS - Tauri (like electron but not bulky) - Windows, Linux @@ -43,6 +43,7 @@ We have: - Terminal (just run it in your terminal) - Windows, macOS, Linux - DS (touch the google balls on your ds! thanks to @DieGo367) - DS - ActionScript 2/3 (google balls for those who don't have html5 capable browsers in the big 2026 for whatever reason! thanks to @leap0x7b) - Flash, non-HTML5 browsers +- Nintendo Switch (native libnx port with touch and controller support) - Nintendo Switch # How 2 download: See [the docs](docs/installing.md) diff --git a/docs/compiling.md b/docs/compiling.md index ca433e7..f70aa99 100644 --- a/docs/compiling.md +++ b/docs/compiling.md @@ -75,6 +75,20 @@ your on your own gang install [BlocksDS](https://blocksds.skylyrac.net/docs/setup/) and then run `make` in the `ds` directory +# Switch + +Get the [toolchain from devkitPro](https://switchbrew.org/wiki/Setting_up_Development_Environment) then run: + +```bash +make +``` + +or if you don't feel like doing that you can install Docker instead and build it inside [a container](https://hub.docker.com/r/devkitpro/devkita64) with: + +```bash +make -f docker.mk build +``` + # Tizen its in installing.md :v: :joy: diff --git a/docs/installing.md b/docs/installing.md index a0f0b88..258a8b6 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -199,6 +199,10 @@ put it on your usb go to package installer select usb install pkg boom bobs your ## DS [Download .nds](https://github.com/weeniemount/googleballs-app/releases/latest/download/google-balls-ds.nds) +## Switch + +[Download .nro](https://github.com/weeniemount/googleballs-app/releases/latest/download/google-balls-switch.nro) + ## Flash [Download AS2 .swf](https://github.com/weeniemount/googleballs-app/releases/latest/download/googleballs-flash-as2.swf) [Download AS3 .swf](https://github.com/weeniemount/googleballs-app/releases/latest/download/googleballs-flash-as3.swf) diff --git a/switch/.gitignore b/switch/.gitignore new file mode 100644 index 0000000..bd8838f --- /dev/null +++ b/switch/.gitignore @@ -0,0 +1,5 @@ +build/ +*.elf +*.map +*.nacp +*.nro diff --git a/switch/Makefile b/switch/Makefile new file mode 100644 index 0000000..0533b40 --- /dev/null +++ b/switch/Makefile @@ -0,0 +1,140 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. +#--------------------------------------------------------------------------------- +TARGET := google-balls +BUILD := build +SOURCES := source +DATA := data +INCLUDES := include + +APP_TITLE := Google Balls +APP_AUTHOR := weenie +APP_VERSION := 1.0.0 +ICON := icon.jpg + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE + +CFLAGS := -g -Wall -O2 -ffunction-sections \ + $(ARCH) $(DEFINES) +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) \ + -Wl,-Map,$(notdir $*.map) + +LIBS := -lnx + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +ifeq ($(strip $(CPPFILES)),) + export LD := $(CC) +else + export LD := $(CXX) +endif + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export NROFLAGS += --icon=$(CURDIR)/$(ICON) +export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp + +.PHONY: all clean $(BUILD) + +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf + +else + +DEPENDS := $(OFILES:.o=.d) + +.PHONY: all + +all: $(OUTPUT).nro + +$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp +$(OUTPUT).elf: $(OFILES) +$(OFILES_SRC): $(HFILES_BIN) + +%.bin.o %_bin.h: %.bin + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +endif +#--------------------------------------------------------------------------------- diff --git a/switch/docker.mk b/switch/docker.mk new file mode 100644 index 0000000..839ca4f --- /dev/null +++ b/switch/docker.mk @@ -0,0 +1,24 @@ +DEVKITA64_IMAGE ?= devkitpro/devkita64:20260219 +PROJECT_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +DOCKER_RUN := docker run --rm \ + --user $(shell id -u):$(shell id -g) \ + --env HOME=/tmp \ + --volume "$(PROJECT_DIR):/project" \ + --workdir /project \ + $(DEVKITA64_IMAGE) + +.PHONY: build clean shell + +build: + $(DOCKER_RUN) make + +clean: + $(DOCKER_RUN) make clean + +shell: + docker run --rm -it \ + --user $(shell id -u):$(shell id -g) \ + --env HOME=/tmp \ + --volume "$(PROJECT_DIR):/project" \ + --workdir /project \ + $(DEVKITA64_IMAGE) bash diff --git a/switch/icon.jpg b/switch/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..50f54540c969161e6543319cb85e9fc1b2e82bfa GIT binary patch literal 10770 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 +#include +#include +#include + +#include + +namespace +{ + + constexpr int kScreenWidth = 1280; + constexpr int kScreenHeight = 720; + constexpr float kPointScale = 2.55f; + constexpr float kRepelDistance = 155.0f; + constexpr float kRepelDistanceSquared = kRepelDistance * kRepelDistance; + constexpr float kAnalogSpeed = 14.0f; + constexpr float kDpadSpeed = 9.0f; + constexpr int kStickDeadzone = 4500; + constexpr float kOffscreenMargin = 240.0f; + constexpr float kMaximumVelocity = 80.0f; + + struct Vec3 + { + float x; + float y; + float z; + }; + + struct PointData + { + int x; + int y; + int radius; + u8 red; + u8 green; + u8 blue; + }; + + constexpr std::array kPointData{{ + {202, 78, 9, 0xed, 0x9d, 0x33}, + {348, 83, 9, 0xd4, 0x4d, 0x61}, + {256, 69, 9, 0x4f, 0x7a, 0xf2}, + {214, 59, 9, 0xef, 0x9a, 0x1e}, + {265, 36, 9, 0x49, 0x76, 0xf3}, + {300, 78, 9, 0x26, 0x92, 0x30}, + {294, 59, 9, 0x1f, 0x9e, 0x2c}, + {45, 88, 9, 0x1c, 0x48, 0xdd}, + {268, 52, 9, 0x2a, 0x56, 0xea}, + {73, 83, 9, 0x33, 0x55, 0xd8}, + {294, 6, 9, 0x36, 0xb6, 0x41}, + {235, 62, 9, 0x2e, 0x5d, 0xef}, + {353, 42, 8, 0xd5, 0x37, 0x47}, + {336, 52, 8, 0xeb, 0x67, 0x6f}, + {208, 41, 8, 0xf9, 0xb1, 0x25}, + {321, 70, 8, 0xde, 0x36, 0x46}, + {8, 60, 8, 0x2a, 0x59, 0xf0}, + {180, 81, 8, 0xeb, 0x9c, 0x31}, + {146, 65, 8, 0xc4, 0x17, 0x31}, + {145, 49, 8, 0xd8, 0x20, 0x38}, + {246, 34, 8, 0x5f, 0x8a, 0xf8}, + {169, 69, 8, 0xef, 0xa1, 0x1e}, + {273, 99, 8, 0x2e, 0x55, 0xe2}, + {248, 120, 8, 0x41, 0x67, 0xe4}, + {294, 41, 8, 0x0b, 0x99, 0x1a}, + {267, 114, 8, 0x48, 0x69, 0xe3}, + {78, 67, 8, 0x30, 0x59, 0xe3}, + {294, 23, 8, 0x10, 0xa1, 0x1d}, + {117, 83, 8, 0xcf, 0x40, 0x55}, + {137, 80, 8, 0xcd, 0x43, 0x59}, + {14, 71, 8, 0x28, 0x55, 0xea}, + {331, 80, 8, 0xca, 0x27, 0x3c}, + {25, 82, 8, 0x26, 0x50, 0xe1}, + {233, 46, 8, 0x4a, 0x7b, 0xf9}, + {73, 13, 8, 0x3d, 0x65, 0xe7}, + {327, 35, 6, 0xf4, 0x78, 0x75}, + {319, 46, 6, 0xf3, 0x67, 0x64}, + {256, 81, 6, 0x1d, 0x4e, 0xeb}, + {244, 88, 6, 0x69, 0x8b, 0xf1}, + {194, 32, 6, 0xfa, 0xc6, 0x52}, + {97, 56, 6, 0xee, 0x52, 0x57}, + {105, 75, 6, 0xcf, 0x2a, 0x3f}, + {42, 4, 6, 0x56, 0x81, 0xf5}, + {10, 27, 6, 0x45, 0x77, 0xf6}, + {166, 55, 6, 0xf7, 0xb3, 0x26}, + {266, 88, 6, 0x2b, 0x58, 0xe8}, + {178, 34, 6, 0xfa, 0xcb, 0x5e}, + {100, 65, 6, 0xe0, 0x2e, 0x3d}, + {343, 32, 6, 0xf1, 0x6d, 0x6f}, + {59, 5, 6, 0x50, 0x7b, 0xf2}, + {27, 9, 6, 0x56, 0x83, 0xf7}, + {233, 116, 6, 0x31, 0x58, 0xe2}, + {123, 32, 6, 0xf0, 0x69, 0x6c}, + {6, 38, 6, 0x37, 0x69, 0xf6}, + {63, 62, 6, 0x60, 0x84, 0xef}, + {6, 49, 6, 0x2a, 0x5c, 0xf4}, + {108, 36, 6, 0xf4, 0x71, 0x6e}, + {169, 43, 6, 0xf8, 0xc2, 0x47}, + {137, 37, 6, 0xe7, 0x46, 0x53}, + {318, 58, 6, 0xec, 0x41, 0x47}, + {226, 100, 5, 0x48, 0x76, 0xf1}, + {101, 46, 5, 0xef, 0x5c, 0x5c}, + {226, 108, 5, 0x25, 0x52, 0xea}, + {17, 17, 5, 0x47, 0x79, 0xf7}, + {232, 93, 5, 0x4b, 0x78, 0xf1}, + }}; + + class Point + { + public: + Point() = default; + Point(float x, float y, float radius, u8 red, u8 green, u8 blue) + : position_{x, y, 1.0f}, original_{x, y, 1.0f}, target_{x, y, 1.0f}, velocity_{0, 0, 0}, + base_radius_(radius), radius_(radius), color_(RGBA8_MAXALPHA(red, green, blue)) {} + + void update(float cursor_x, float cursor_y, float delta_seconds) + { + const float dx = cursor_x - position_.x; + const float dy = cursor_y - position_.y; + if (dx * dx + dy * dy < kRepelDistanceSquared) + { + target_.x = position_.x - dx; + target_.y = position_.y - dy; + } + else + { + target_.x = original_.x; + target_.y = original_.y; + } + + const float time_scale = delta_seconds / 0.03f; + updateAxis(position_.x, target_.x, velocity_.x, time_scale, 0.1f, 0.01f); + updateAxis(position_.y, target_.y, velocity_.y, time_scale, 0.1f, 0.01f); + const float origin_dx = original_.x - position_.x; + const float origin_dy = original_.y - position_.y; + target_.z = std::sqrt(origin_dx * origin_dx + origin_dy * origin_dy) / 100.0f + 1.0f; + updateAxis(position_.z, target_.z, velocity_.z, time_scale, 0.01f, 0.001f); + + if (!std::isfinite(position_.x) || !std::isfinite(position_.y) || + !std::isfinite(position_.z) || !std::isfinite(velocity_.x) || + !std::isfinite(velocity_.y) || !std::isfinite(velocity_.z)) + { + reset(); + return; + } + + position_.x = std::clamp(position_.x, -kOffscreenMargin, + kScreenWidth + kOffscreenMargin); + position_.y = std::clamp(position_.y, -kOffscreenMargin, + kScreenHeight + kOffscreenMargin); + velocity_.x = std::clamp(velocity_.x, -kMaximumVelocity, kMaximumVelocity); + velocity_.y = std::clamp(velocity_.y, -kMaximumVelocity, kMaximumVelocity); + velocity_.z = std::clamp(velocity_.z, -kMaximumVelocity, kMaximumVelocity); + radius_ = std::max(1.0f, std::min(base_radius_ * position_.z, 120.0f)); + } + + void reset() + { + position_ = original_; + target_ = original_; + velocity_ = {0, 0, 0}; + radius_ = base_radius_; + } + + float x() const { return position_.x; } + float y() const { return position_.y; } + float radius() const { return radius_; } + u32 color() const { return color_; } + + private: + static void updateAxis(float &position, float target, float &velocity, float time_scale, + float position_threshold, float velocity_threshold) + { + const float delta = target - position; + velocity += delta * 0.1f * time_scale; + velocity *= std::pow(0.8f, time_scale); + if (std::fabs(delta) < position_threshold && std::fabs(velocity) < velocity_threshold) + { + position = target; + velocity = 0; + } + else + { + position += velocity * time_scale; + } + } + + Vec3 position_{}; + Vec3 original_{}; + Vec3 target_{}; + Vec3 velocity_{}; + float base_radius_ = 0; + float radius_ = 0; + u32 color_ = 0; + }; + + class App + { + public: + App() + { + const float offset_x = (kScreenWidth - 353.0f * kPointScale) * 0.5f; + const float offset_y = (kScreenHeight - 120.0f * kPointScale) * 0.5f; + for (std::size_t i = 0; i < kPointData.size(); ++i) + { + const PointData &p = kPointData[i]; + points_[i] = Point(offset_x + p.x * kPointScale, offset_y + p.y * kPointScale, + p.radius * kPointScale, p.red, p.green, p.blue); + } + } + + bool initialize() + { + const Result result = framebufferCreate(&framebuffer_, nwindowGetDefault(), kScreenWidth, + kScreenHeight, PIXEL_FORMAT_RGBA_8888, 2); + if (R_FAILED(result)) + return false; + framebufferMakeLinear(&framebuffer_); + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + padInitializeDefault(&pad_); + return true; + } + + void run() + { + u64 last_tick = armGetSystemTick(); + while (appletMainLoop()) + { + if (!handleInput()) + break; + const u64 current_tick = armGetSystemTick(); + const float delta_seconds = std::min( + static_cast(armTicksToNs(current_tick - last_tick)) / 1000000000.0f, + 0.1f); + last_tick = current_tick; + for (Point &point : points_) + point.update(cursor_x_, cursor_y_, delta_seconds); + render(); + } + } + + void shutdown() { framebufferClose(&framebuffer_); } + + private: + bool handleInput() + { + padUpdate(&pad_); + const u64 down = padGetButtonsDown(&pad_); + const u64 held = padGetButtons(&pad_); + if (down & HidNpadButton_Plus) + return false; + if (down & HidNpadButton_Minus) + dark_background_ = !dark_background_; + if (down & HidNpadButton_A) + for (Point &point : points_) + point.reset(); + + HidTouchScreenState touch{}; + if (hidGetTouchScreenStates(&touch, 1) > 0 && touch.count > 0) + { + cursor_x_ = touch.touches[0].x; + cursor_y_ = touch.touches[0].y; + cursor_active_ = true; + } + else + { + bool moved = moveFromStick(padGetStickPos(&pad_, 0)); + moved |= moveFromStick(padGetStickPos(&pad_, 1)); + if (held & HidNpadButton_Left) + { + cursor_x_ -= kDpadSpeed; + moved = true; + } + if (held & HidNpadButton_Right) + { + cursor_x_ += kDpadSpeed; + moved = true; + } + if (held & HidNpadButton_Up) + { + cursor_y_ -= kDpadSpeed; + moved = true; + } + if (held & HidNpadButton_Down) + { + cursor_y_ += kDpadSpeed; + moved = true; + } + cursor_active_ |= moved; + } + + cursor_x_ = std::max(0.0f, std::min(cursor_x_, float(kScreenWidth - 1))); + cursor_y_ = std::max(0.0f, std::min(cursor_y_, float(kScreenHeight - 1))); + return true; + } + + bool moveFromStick(const HidAnalogStickState &stick) + { + if (std::abs(stick.x) <= kStickDeadzone && std::abs(stick.y) <= kStickDeadzone) + return false; + cursor_x_ += float(stick.x) / 32768.0f * kAnalogSpeed; + cursor_y_ -= float(stick.y) / 32768.0f * kAnalogSpeed; + return true; + } + + void render() + { + u32 stride = 0; + u32 *pixels = static_cast(framebufferBegin(&framebuffer_, &stride)); + const int pitch = stride / sizeof(u32); + const u32 background = dark_background_ ? RGBA8_MAXALPHA(26, 26, 26) + : RGBA8_MAXALPHA(255, 255, 255); + for (int y = 0; y < kScreenHeight; ++y) + std::fill_n(pixels + y * pitch, kScreenWidth, background); + for (const Point &point : points_) + drawCircle(pixels, pitch, std::lround(point.x()), std::lround(point.y()), + std::lround(point.radius()), point.color()); + if (cursor_active_) + drawCursor(pixels, pitch, int(cursor_x_), int(cursor_y_)); + framebufferEnd(&framebuffer_); + } + + static void drawCircle(u32 *pixels, int pitch, int cx, int cy, int radius, u32 color) + { + radius = std::clamp(radius, 1, 120); + if (cx + radius < 0 || cx - radius >= kScreenWidth || + cy + radius < 0 || cy - radius >= kScreenHeight) + { + return; + } + + const int min_y = std::max(-radius, -cy); + const int max_y = std::min(radius, kScreenHeight - 1 - cy); + if (min_y > max_y) + return; + + const int radius_squared = radius * radius; + for (int y = min_y; y <= max_y; ++y) + { + const int remaining = radius_squared - y * y; + if (remaining < 0) + continue; + const int half_width = std::sqrt(float(remaining)); + const int start = std::max(0, cx - half_width); + const int end = std::min(kScreenWidth - 1, cx + half_width); + if (start > end) + continue; + std::fill(pixels + (cy + y) * pitch + start, + pixels + (cy + y) * pitch + end + 1, color); + } + } + + void drawCursor(u32 *pixels, int pitch, int x, int y) const + { + const u32 outline = dark_background_ ? RGBA8_MAXALPHA(255, 255, 255) + : RGBA8_MAXALPHA(0, 0, 0); + drawLine(pixels, pitch, x - 13, y, x - 4, y, outline); + drawLine(pixels, pitch, x + 4, y, x + 13, y, outline); + drawLine(pixels, pitch, x, y - 13, x, y - 4, outline); + drawLine(pixels, pitch, x, y + 4, x, y + 13, outline); + drawCircle(pixels, pitch, x, y, 2, RGBA8_MAXALPHA(128, 128, 128)); + } + + static void drawLine(u32 *pixels, int pitch, int x0, int y0, int x1, int y1, u32 color) + { + if (y0 == y1 && y0 >= 0 && y0 < kScreenHeight) + { + const int start = std::max(0, std::min(x0, x1)); + const int end = std::min(kScreenWidth - 1, std::max(x0, x1)); + if (start <= end) + std::fill(pixels + y0 * pitch + start, pixels + y0 * pitch + end + 1, color); + } + else if (x0 == x1 && x0 >= 0 && x0 < kScreenWidth) + { + const int start = std::max(0, std::min(y0, y1)); + const int end = std::min(kScreenHeight - 1, std::max(y0, y1)); + for (int y = start; y <= end; ++y) + pixels[y * pitch + x0] = color; + } + } + + Framebuffer framebuffer_{}; + PadState pad_{}; + std::array points_{}; + float cursor_x_ = kScreenWidth * 0.5f; + float cursor_y_ = kScreenHeight * 0.5f; + bool cursor_active_ = true; + bool dark_background_ = false; + }; + +} // namespace + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + App app; + if (!app.initialize()) + return EXIT_FAILURE; + app.run(); + app.shutdown(); + return EXIT_SUCCESS; +} From 8843e2157a3144f6ea2503c08b068531e1285a11 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Thu, 17 Sep 2026 20:27:22 -0700 Subject: [PATCH 2/2] docs(switch): make tagline funnier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5dd9ed..7a96ca4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ We have: - Terminal (just run it in your terminal) - Windows, macOS, Linux - DS (touch the google balls on your ds! thanks to @DieGo367) - DS - ActionScript 2/3 (google balls for those who don't have html5 capable browsers in the big 2026 for whatever reason! thanks to @leap0x7b) - Flash, non-HTML5 browsers -- Nintendo Switch (native libnx port with touch and controller support) - Nintendo Switch +- Nintendo Switch (touch the balls on your switch) - Nintendo Switch # How 2 download: See [the docs](docs/installing.md)