{ "APP": { "TITLE": "Playground", "COPYRIGHT": "All rights reserved.", "AI_NOTICE": "Built with an AI-assisted engineering workflow.", "AI_NOTICE_LINK": "How I build with AI" }, "TOPBAR": { "ABOUT": "About me", "IMPRINT": "Impressum", "PROJECTS": "Projects", "ALGORITHMS": "Algorithms", "STOPWATCH": "Stopwatch", "SETTINGS": "Settings", "LANGUAGE": "Language", "APPEARANCE": "Appearance" }, "THEME": { "LIGHT": "Light", "DARK": "Dark" }, "LANG": { "LABEL": "Language", "EN": "English", "DE": "Deutsch" }, "ABOUT": { "ALT": { "PROFILE": "Profile photo of Andreas Dahm" }, "HELLO": "Hello, I’m Andreas.", "LEAD": "I have been working as a software developer since 2010. Most of that time went into two fairly different corners. One is real-time 3D simulation: cloth physics, collision detection, GPU shaders. The other is cloud architecture for larger enterprise platforms. I led a small backend team for a few years, planned the move of a big legacy monolith towards microservices, and released a game on Steam together with my brother. Lately a lot of my time goes into tooling around AI-assisted development, mostly into making the output reviewable instead of just fast. In my free time I build games and mess around with algorithms. Most of that ends up somewhere on this site.", "ROLE": "Senior Software Engineer · Cloud Architecture & 3D Simulation", "LOCATION": "Munich · Remote", "VIEW_PROJECTS": "View projects", "SECTION": { "SKILLS": "Skills & Stack", "LANGUAGES": "Languages & Frameworks", "ARCH_CLOUD": "Architecture & Cloud", "SIM_ALGO": "Simulation & Algorithms", "AI_ENG": "AI-Assisted Engineering", "EXPERIENCE": "Experience", "PROJECTS": "Projects", "EDUCATION": "Education", "SHOW_EARLIER_XP": "Show earlier positions", "SHOW_EARLIER_EDU": "Show earlier education", "SHOW_LESS": "Show less" }, "SKILLS": { "JAVA": "Java", "SPRING": "Spring Boot", "ANGULAR": "Angular", "VUE": "Vue.js", "REACT": "React", "DOCKER": "Docker", "UNITY": "Unity", "PYTHON": "Python", "CSHARP": "C#", "TYPESCRIPT": "TypeScript", "ARCH_MICROSERVICES": "Microservices", "ARCH_CLOUD": "Cloud Architecture", "ENG_ALGO": "Algorithm Design", "ENG_SIM": "3D Simulation", "ENG_GPU": "WebGPU / OpenGL / GLSL", "ENG_PERF": "Performance Optimization", "ENG_3D": "3D-Scanner Tech", "AI_AGENTS": "Claude Code / Agent SDK", "AI_MCP": "MCP servers", "AI_CONTEXT": "Context & prompt engineering", "AI_HARNESS": "Agent harness design", "AI_GATES": "Automated review gates" }, "TOOLS": { "GIT": "Git", "GITHUB": "Github", "GITLAB": "Gitlab", "JENKINS": "Jenkins", "K8S": "Kubernetes / k3d", "POSTGRES": "PostgreSQL", "MONGO": "MongoDB", "GRAFANA": "Grafana/Prometheus", "DOCKER": "Docker" }, "XP": { "COMPANY9": { "COMPANY": "ColorDigital GmbH", "ROLE": "Senior Software Engineer - DMIxOS Team", "TIME": "March 2026 – present", "HIGHLIGHTS": { "P1": "Designed and implemented the core cloud architecture.", "P2": "Full-stack development and optimization of new and existing system components.", "P3": "Technical integration of partner companies into the proprietary ecosystem." } }, "COMPANY8": { "COMPANY": "Teraport GmbH", "ROLE": "Senior Software Developer / Architect", "TIME": "Feb. 2024 – Feb. 2026", "HIGHLIGHTS": { "P1": "Architecture and implementation of database connectivity using Hibernate 6.x.", "P2": "Design and development of a full-stack web application for collision analysis (Angular + Spring Boot + Docker).", "P3": "Development of MIDO, a cost calculation tool for injection molding, die casting and stamping tools: 3D layout of the mold and its cavities, demolding direction, undercut detection and a cost breakdown by tool component." } }, "COMPANY7": { "COMPANY": "ColorDigital GmbH", "ROLE": "Lead Software Developer", "TIME": "Mar. 2023 – Dec. 2023", "HIGHLIGHTS": { "P1": "Planning a new cloud architecture to migrate a large legacy monolith to a modern microservice-based platform.", "P2": "Led the development team of three to four backend engineers.", "P3": "Collaborated with partners and customers on integrating their systems with the DMIx." } }, "COMPANY6": { "COMPANY": "ColorDigital GmbH", "ROLE": "Senior Software Developer", "TIME": "Mar. 2021 – Mar. 2023", "HIGHLIGHTS": { "P1": "Worked on the DMIx core cloud, including the implementation of an Elasticsearch-based real-time search module.", "P2": "Implementation of a new browser tool called 'Moodboard' (Node.js + Vue.js).", "P3": "Development of 'PAX', a tool for data exchange between the DMIx cloud and customer PLM/ERP systems, including CI/CD integration." } }, "COMPANY5": { "COMPANY": "Assyst GmbH", "ROLE": "Team leadership", "TIME": "Sep. 2015 – Feb. 2021", "HIGHLIGHTS": { "P1": "Team lead of the Vidya software department, including disciplinary responsibility and interviews.", "P2": "Led several projects with external partners and freelancers.", "P3": "Managed strategic initiatives in 3D software development." } }, "COMPANY4": { "COMPANY": "Assyst GmbH", "ROLE": "Software engineer", "TIME": "Apr. 2010 – Feb. 2021", "HIGHLIGHTS": { "P1": "Worked on core technologies such as algorithms, simulation, rendering, and collision detection.", "P2": "Used technologies including Java, OpenCL, OpenGL, GLSL shaders, C++, and REST interfaces.", "P3": "Participated in national and international research projects (EU7 and ZIM)." } }, "COMPANY3": { "COMPANY": "Assyst GmbH", "ROLE": "Internship and Diploma", "TIME": "Apr. 2009 – Apr. 2010", "HIGHLIGHTS": { "P1": "Implemented production features in the real-time cloth simulation software Vidya.", "P2": "Conducted research and literature review for the diploma thesis.", "P3": "Successfully implemented the diploma topic in Vidya." } }, "COMPANY2": { "COMPANY": "TH Bingen (University of Applied Sciences)", "ROLE": "Tutorial for fundamentals of Java programming", "TIME": "Apr. 2008 – Aug. 2008", "HIGHLIGHTS": { "P1": "Preparation of practical and theoretical course material.", "P2": "Held a 20-hour lecture series for the bioinformatics programme.", "P3": "Provided assistance during practical programming exercises." } }, "COMPANY1": { "COMPANY": "TH Bingen (University of Applied Sciences)", "ROLE": "Research Assistant", "TIME": "Oct. 2007 – Apr. 2008", "HIGHLIGHTS": { "P1": "Evaluation of various Web 2.0 technologies.", "P2": "Implementation of a Java-based interface for Microsoft Excel.", "P3": "Integration of the MIT Exhibit framework and preparation for CeBIT 2008." } }, "COMPANY0": { "COMPANY": "Chamaeleon AG", "ROLE": "Training as an IT specialist in application development", "TIME": "Jul. 2002 – Jun. 2005", "HIGHLIGHTS": { "P1": "Development in PERL, PHP and ASP.", "P2": "Porting, maintenance and reengineering of existing software.", "P3": "Regular performance of system tests and quality controls, as well as their documentation." } } }, "PROJECT": { "TBP": { "TITLE": "To Be Played", "DESCRIPTION": "A backlog tracker for Steam and PlayStation that syncs my libraries, keeps status, rating and notes, and suggests what to play next for the time I have. Three self-hosted services: auth, core and a React frontend.", "LINK_EXTERNAL": "tbp.andreas-dahm.eu", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "Steam and PlayStation sync of games, playtime, achievements and trophies, plus time to beat from IGDB.", "P2": "\"What to play next\": seven rows of suggestions, filtered by a time budget; a mood filter for the random picks.", "P3": "Auth signs the tokens with Ed25519; Core fetches the public key by key id and only verifies." } }, "CLAUDE_BOX": { "TITLE": "Claude-in-a-Box", "DESCRIPTION": "A development setup in a container where AI agents write code under fixed rules. A pipeline of small agents clarifies, plans, implements, simplifies and reviews, and I say yes to the plan and to every commit. Hooks outside the agents block red builds and reads of secrets.", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "One agent per job, each with a fresh context and a model that fits the job. Hand-over runs through files, not through the chat.", "P2": "A commit hook runs the build, the tests and a secret scan and blocks the commit when anything is red.", "P3": "An eval suite measures every change to rules or agents against fixed tasks with hidden checks." } }, "P2": { "TITLE": "Playground", "DESCRIPTION": "This site. A portfolio and a place to try algorithms in the browser: pathfinding, sorting, fractals in GLSL, and a cloth and a double pendulum simulation as WebGPU compute shaders. Angular, German and English, built and shipped by my own CI.", "LINK_EXTERNAL": "andreas-dahm.eu", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "Visualizations from Dijkstra to raymarched 3D fractals, all on the client.", "P2": "Two WebGPU compute simulations (cloth with XPBD, double pendulum) with a CPU or WebGL fallback.", "P3": "Gitea Actions pipeline: lint, i18n check, tests, Lighthouse, Docker image to my own registry." } }, "P1": { "TITLE": "El Mucho", "DESCRIPTION": "My first published game, a turn-based tactical RPG, made with my brother over about three years and released on Steam in 2022. He designed it, I built it.", "LINK_EXTERNAL": "Steam Store", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "All the code in Unity (C#), the builds, the Steam setup and the release.", "P2": "16 missions, achievements, three languages (EN, DE, ES).", "P3": "A custom AI for the enemy units and my own A* pathfinding on the map grid." } }, "P0": { "TITLE": "Game Jams", "DESCRIPTION": "Three 48-hour jam games from the BeansJam series, in teams of two or three. I was the programmer and lead each time. All of them are playable on itch.io.", "LINK_EXTERNAL": "Itch.io Collection", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "Peter Blue and the carnival of death (2018), Arkonauts (2020), Sloth Bros (2022).", "P2": "Friday to Sunday: a playable game in 48 hours, every time.", "P3": "A cheap way to find out whether a game idea works before committing years to it." } }, "DIPLOMA": { "TITLE": "Diploma thesis - Collision detection and handling of complex garments.", "DESCRIPTION": "Self-intersections inside one garment part, such as a pleated skirt, in the cloth simulation Vidya at assyst. I adapted Intersection Contour Minimization to Vidya's k-DOP hierarchy and extended the response with mass and velocity. FH Bingen, March 2010.", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "Detection on the existing BVH with k-DOPs; one shared hierarchy cut update and traversal time in half.", "P2": "Compared four published methods and took the global Intersection Contour Minimization variant.", "P3": "About 20 % faster than the existing self-collision detector plus Layering, planned for productive use." } }, "TRIBBLE": { "TITLE": "Homeserver 'Tribble'", "DESCRIPTION": "A mini PC at home runs Gitea with its Actions runner and registry, plus Jellyfin, Paperless-ngx and AdGuard Home. A VPS serves the public sites and pulls new images over Tailscale with Watchtower. Running since May 2024.", "LINK_INTERNAL": "Project details", "HIGHLIGHTS": { "P1": "Gitea CI builds and tests this site and To Be Played at home; Watchtower deploys the images on the VPS.", "P2": "Traefik for TLS and routing, Tailscale between the two machines.", "P3": "The hard parts were DNS and making the setup repeatable, not the services themselves." } } }, "EDUCATION": { "E6": { "WHERE": "FH Bingen (University of Applied Sciences)", "WHEN": "2006 – 2010", "WHAT": "Diploma (FH), Applied Computer Science (final grade 1.4)" }, "E5": { "WHERE": "FH Koblenz (University of Applied Sciences)", "WHEN": "2005 – 2006", "WHAT": "1 semester of Engineering Informatics" }, "E4": { "WHERE": "BBS Montabaur", "WHEN": "2002 – 2005", "WHAT": "Apprenticeship as IT Specialist for Application Development" }, "E3": { "WHERE": "BBS Andernach – Higher Vocational School (Computer Science)", "WHEN": "2000 – 2002", "WHAT": "University entrance qualification (Fachhochschulreife)" }, "E2": { "WHERE": "BBS Andernach – Vocational School for Technology (Electrical Engineering)", "WHEN": "1998 – 2000", "WHAT": "Secondary school certificate (Realschulabschluss)" }, "E1": { "WHERE": "Hauptschule Hinter Burg Mayen (Albert Schweitzer Realschule Plus)", "WHEN": "1995 – 1998", "WHAT": "Lower secondary school certificate (Hauptschulabschluss)" }, "E0": { "WHERE": "Albert Schweitzer Realschule Mayen", "WHEN": "1993 – 1995", "WHAT": "No degree (transferred to lower secondary school)" } } }, "PROJECTS": { "DOWNLOAD": "Download", "READ_MORE": "Read More", "DIAGRAM_ALT": "Architecture diagram", "LINK_TO_PROJECT": "To the project", "CLOSE": "Close", "SECTION": { "TECHNICAL": "Technical Challenges", "LEARNINGS": "Learnings & Soft Skills", "DECISIONS": "Decisions", "RESULT": "Result" }, "PLAYGROUND": { "TITLE": "Playground", "SHORT_DESCRIPTION": "This site: a portfolio with algorithm visualizations that run in the browser, some of them on the GPU.", "INTRODUCTION": "This site is where I try things out. I wanted to see how far a plain browser gets with the algorithms I like, so every visualization runs on the client: pathfinding, maze generation, sorting, Conway's Game of Life, fractals in GLSL shaders, and two physics simulations that run as compute shaders on WebGPU. The rest is a normal Angular app with German and English texts, a dark mode, and a pipeline that takes every push to main all the way to the live server.", "BULLET_1": "Pathfinding with Dijkstra and A*, maze generation with Prim and Kruskal, five sorting algorithms with sound, Conway's Game of Life and a four-color map.", "BULLET_2": "2D fractals (Mandelbrot, Julia, Burning Ship, Newton) in one GLSL shader, and 3D fractals (Mandelbulb, Mandelbox, Julia-Bulb) by raymarching with normals from the distance field.", "BULLET_3": "A double pendulum and a cloth simulation run as compute shaders on WebGPU. Without WebGPU they fall back to the CPU or WebGL and say so in a snackbar.", "BULLET_4": "Angular with Material and Tailwind, signals for state, German and English. Every route is prerendered at build time into its own HTML with its own title and meta tags, so a shared link gets a proper preview. In the browser, Angular and a service worker (PWA) take over from the static page.", "BULLET_5": "Privacy by design: the fonts come from the site itself, there are no cookies, no tracking and no access log on the server. Only language and theme are kept in the browser's local storage.", "BULLET_6": "Operations: CI on Gitea Actions on my home server Tribble runs lint, i18n check, tests, the production build, a prerender check and Lighthouse. A push to main builds a Docker image; on the VPS, Watchtower pulls it, nginx serves the static files and Traefik handles HTTPS. Gitea itself is reached through Traefik over Tailscale.", "CHALLENGE_1": "The cloth solver on the GPU. XPBD constraints that share a particle cannot be solved in parallel without a race. I group the constraints by graph coloring into eight phases, so inside one phase no two constraints touch the same particle and each phase runs fully in parallel.", "CHALLENGE_2": "Not every browser has WebGPU. Both simulations have a CPU or WebGL path and a snackbar that says the fallback is active, so the page works everywhere and nobody wonders why it is slower.", "CHALLENGE_3": "Prerendering a SPA that already existed. At build time every page is rendered in Node, and there is no WebGPU, no canvas for Babylon.js and no localStorage there. Browser-only code had to be guarded component by component until the build ran through. The client-side shell index.csr.html stays, because the service worker and the 404 page still need it.", "LEARNING_1": "Compute shaders change how you think about an algorithm. The data layout and the order of writes matter more than the math.", "LEARNING_2": "A small CI with a key-parity check and a Lighthouse run catches the boring mistakes, a missing translation or a slow page, before I see them on the live site.", "LEARNING_3": "A privacy page is a spec for the server. Once it says there is no access log and no connection to Google, nginx must keep no log and the fonts must ship with the site. Writing it down first made those decisions for me.", "DECISION_1": "Prerender at build time instead of client rendering or an SSR server. Every route becomes its own HTML file at build time, and nginx serves it as a static file; no Node process runs on the VPS. The reason was the link preview: a shared link needs title and meta tags in the HTML, not only after the app has loaded. A Node server would have been too much effort for a site whose content runs in the browser anyway.", "DECISION_2": "Vitest with jsdom instead of Karma with Chrome. Karma needed a headless Chrome in the CI container; Vitest runs with no browser. The downside: jsdom applies no real CSS, so two tests on the loaded fonts were dropped.", "DECISION_3": "Tailwind without preflight instead of SCSS-first or all of Tailwind. The styles moved from one big styles.scss to utility classes in the templates, so a style sits next to the markup it affects; SCSS stays only for the Material theme and its overrides. Tailwind's own reset (preflight) stays out, so the reset of Angular Material keeps priority.", "RESULT": "The site runs the way the privacy page promises: no cookies, no tracking, no Google Fonts request, and the build fails as soon as one appears. A push to main is live within a few minutes, with no manual step. Shared links now show a title and a preview, and CI catches missing translations and a missing title or canonical tag before I see them on the live site." }, "TRIBBLE": { "TITLE": "Self-Hosted Infrastructure", "SHORT_DESCRIPTION": "Home server for CI and private services, plus a VPS for the public sites.", "INTRODUCTION": "Two machines. At home a mini PC called \"Tribble\" runs Gitea with its Actions runner and container registry, plus the private services. A Hostinger VPS serves the public sites, this one included. A git push builds the image at home, the VPS pulls it over Tailscale with Watchtower, and Traefik does TLS and routing. It has been running since May 2024.", "BULLET_1": "Hardware: a KODLIX GD50 mini PC with an 8-core Intel Core i5, 32 GB RAM and a 512 GB SSD.", "BULLET_2": "Gitea for git, the Actions runner and the container registry. This site and To Be Played are built and tested there.", "BULLET_3": "Private services: Jellyfin for media, Paperless-ngx for documents, AdGuard Home for DNS and ad blocking, Traefik in front.", "BULLET_4": "Tailscale links the two machines. The VPS fetches new images from the registry at home over it, and Watchtower swaps the running container.", "CHALLENGE_1": "DNS. Names have to resolve to the right address from inside the house, over Tailscale and from the public internet, and AdGuard Home sits in the middle of it. Getting that right took longer than any single service.", "CHALLENGE_2": "Getting everything to run at the same time, and then making it repeatable. The first setup was a long list of manual steps. I automated it so the server can be rebuilt without me remembering each one.", "LEARNING_1": "Self-hosting is cheap in money and expensive in time. Most of the time goes into the parts nobody sees: DNS, certificates and updates.", "LEARNING_2": "A git push that ends in a running container on the VPS, with no manual step in between, is the one part of this setup I would not give up.", "DECISION_1": "The VPS is the only public entrance. Traefik on the VPS forwards the names hosted at home over Tailscale to the home server, with a few YAML files in its file provider. The alternative would have been a port forward on the home router. This way no port is open at home, and the home server's address stays private.", "DECISION_2": "Watchtower pulls on the VPS, tags stay pinned at home. The VPS checks for new images every minute and replaces only the labelled containers, one at a time; CI needs no access to the VPS for that. The home server gets no \"latest\": every image tag is pinned there, and an update is one manual step per stack, because with \"latest\" a database migration happens as a side effect, and that cannot be undone.", "DECISION_3": "Rebuild by script instead of from memory. bootstrap.sh creates the proxy network and the directories and warns about the DNS port clash between systemd-resolved and AdGuard; rebuild.sh starts the stacks in a fixed order, Traefik first, because alphabetically it would come last. For six Compose stacks, Ansible would cost more effort than it saves. Two shell scripts are enough and easier to read. The scripts cover the infrastructure, not the data.", "RESULT": "Today six stacks run at home: Traefik, AdGuard Home, Gitea with its runner, Jellyfin, Paperless-ngx and Leantime; Nextcloud is switched off again. The VPS runs Traefik, Watchtower and the public sites. Every push of this site and of To Be Played is built at home, pushed to my own registry on Tribble and reaches the VPS with no manual step. In September I updated all stacks in one pass, stack by stack with the pinned tags, and no service went down." }, "EL_MUCHO": { "TITLE": "El Mucho (Steam Release)", "SHORT_DESCRIPTION": "A turn-based tactical RPG that my brother and I released on Steam.", "INTRODUCTION": "El Mucho is a turn-based tactical RPG in a fictional world called Liberika, inspired by Langrisser. My brother and I made it over about three years next to a full-time job and released it on Steam: Early Access in July 2021, full release in February 2022. The split was clear. He had the idea and did the assets, the level design, testing and balancing. I did everything technical: the code in Unity, the builds, the Steam setup and the release.", "BULLET_1": "Unity and C#. One developer for all of the code.", "BULLET_2": "16 missions, achievements and family sharing on Steam, published as Lobo Corp.", "BULLET_3": "English, German and Spanish. The Spanish translation was done by a friend who is a native speaker.", "BULLET_4": "Art and music are partly open source, partly my brother's, partly mine.", "CHALLENGE_1": "Shipping on Steam as a one-person tech team. Builds, store setup, upload and the Early Access phase all went through me. Early Access started in July 2021, the full release followed in February 2022.", "CHALLENGE_2": "Three languages. Every text exists in English, German and Spanish; for Spanish I worked with a native-speaking friend.", "CHALLENGE_3": "The enemy units. I wrote a custom AI for them and my own A* pathfinding on the map grid.", "LEARNING_1": "Finishing takes longer than building. The stretch from Early Access to the release alone took about seven months.", "LEARNING_2": "A clear split of roles kept a two-person team working for three years: he owned the game, I owned the code and the release." }, "GAME_JAMS": { "TITLE": "Game Jams", "SHORT_DESCRIPTION": "Three small games, each made in 48 hours from Friday to Sunday.", "INTRODUCTION": "A game jam gives you a theme on Friday evening and wants a playable game by Sunday. I have done three so far, all in the BeansJam series, in teams of two or three. I was always the programmer and the lead; the others brought ideas, assets and testing. All three games are on itch.io.", "BULLET_1": "Peter Blue and the carnival of death (2018, BeansJam): my first jam.", "BULLET_2": "Arkonauts (2020, Mini Beans Jam 6): space invaders in space.", "BULLET_3": "Sloth Bros (2022, BeansJam '22): theme \"we have to stay awake\", one of 65 entries.", "BULLET_4": "Always 48 hours, Friday to Sunday, in a team of two or three.", "CHALLENGE_1": "Scope. 48 hours is not much, and with sleep it is less. Deciding on Friday what we will not build was the most important decision of each jam.", "CHALLENGE_2": "Being the only programmer. Everything the team came up with went through my keyboard, so an idea had to be cheap to build or it was dropped.", "LEARNING_1": "A playable loop on Saturday beats a better idea on Sunday.", "LEARNING_2": "A jam is where I find out whether a game idea works before spending years on it. El Mucho took three years; a jam game takes a weekend.", "LINK_COLLECTION": "All games on itch.io" }, "DIPLOMA": { "TITLE": "Diploma Thesis: Cloth Self-Collisions", "SHORT_DESCRIPTION": "Detecting and resolving self-intersections in simulated garments, done at assyst for the cloth simulation Vidya (2010).", "INTRODUCTION": "Simulated cloth folds into itself. A pleated skirt, for example, intersects with its own fabric, and once two parts of one garment have passed through each other a normal collision response cannot pull them apart again. Collisions between different layers were already handled in Vidya; self-intersections inside one garment part were not. In my diploma thesis at assyst I adapted Intersection Contour Minimization (Volino et al. 2006) to Vidya and extended it so the response uses mass and velocity. Written in Java, finished in March 2010 at FH Bingen, and planned for productive use.", "BULLET_1": "Detection runs on Vidya's existing bounding volume hierarchy with k-DOPs. For self-tests the traversal skips the inner-node tests it does not need.", "BULLET_2": "The intersection contour is built from segments. A spatial hash joins them even when floating point errors leave small gaps.", "BULLET_3": "The response is my extension: it uses mass and velocity, so the result is physically plausible instead of a plain geometric push.", "BULLET_4": "The parameter h0 is derived from the mean triangle area, so it needs no manual tuning.", "BULLET_5": "The method shares one BVH with Vidya's existing TriangleSelfCollisionDetector.", "CHALLENGE_1": "Cost. The test scene had about 16,000 particles at about 430 ms per step on a 2 GHz Core 2 Duo. Sharing one BVH between my detector and the existing one cut the update and traversal time in half and the overhead by about 80 %. On top of the existing detection the method adds about 5 %.", "CHALLENGE_2": "Choosing the method. I compared Baraff (2003), Bridson (2002) and Volino (2000, 2006) and took the global variant of Intersection Contour Minimization, because it also resolves intersections that already exist.", "CHALLENGE_3": "Known limits, documented: nearly planar surfaces and a vertex that lies exactly on a triangle plane are not handled.", "LEARNING_1": "Reading papers properly. The same problem had four published answers, and the useful work was understanding where each one fails.", "LEARNING_2": "Measure before you claim. The method alone was about 20 % faster than the existing detector plus Layering, and that number only counts because it comes from the same scene on the same machine." }, "CLAUDE_BOX": { "TITLE": "Claude-in-a-Box", "SHORT_DESCRIPTION": "A container setup for an AI coding agent: a pipeline of small agents, one job each, and hooks none of them can get past.", "INTRODUCTION": "An AI coding agent will produce code all day without complaining. The trouble is that what comes out usually looks reasonable even when it is wrong, so all the cost lands in review. I built a Docker setup that puts the agent under the same constraints as anyone else working on the repo: it reads the architecture rules of whatever project it is in, and the build has to pass before a commit goes through. Since then the box has grown into a pipeline. The chat clarifies the task; after that it is one small agent per job, each with a fresh context: one plans, one implements, one simplifies, one reviews. They hand over through files, not through chat, and I say yes at two points, to the plan and to every commit. The hooks around all of this are still the part I keep coming back to, because they are the one thing no agent can talk its way past.", "BULLET_1": "Agent pipeline: clarify (the main session asks sharp questions and writes a spec), plan, implement, simplify, a security check when the change touches something security relevant, review, commit message. Every step after clarify is its own agent with a fresh context and a model that fits the job: Fable for clarify, plan and review, Sonnet for implementing, Opus for simplify and security, Haiku for the commit message. When less than 20% of the weekly Fable budget is left, those steps fall back to Opus on their own, and the statusline shows it.", "BULLET_2": "I stay in the loop at two points: the plan needs my OK before anyone writes code, and every commit needs my yes. The reviewer is critical and has never seen the chat: it runs the gate, writes a failing test for each finding and checks that the feature works, in the browser if needed. Findings go back to the implementer, at most two rounds. An agent that fails to fix the same failure twice stops and returns RED, and I decide. Small changes skip plan and review.", "BULLET_3": "Rules are loaded by detection. A repo with a pom.xml gets the Java and Spring rules, one with a package.json gets the TypeScript rules, a Unity project gets the data-oriented rules, and repos with their own architecture notes get those on top. Each agent reads only the documents its job needs, so the context stays small.", "BULLET_4": "Hooks outside the agents: a compile or type check at the end of every turn, a commit gate that runs the full build, the tests and a secret scan and refuses a red commit, a guard that blocks reads of .env files and keys, and a work-mode guard. Around it a rootless container with pinned toolchains and MCP servers for code graph queries, Gitea and read-only Jira.", "BULLET_5": "Work mode assist, per chat: I write the core code (domain, services, persistence), the agents write scaffolding, boilerplate and tests and review my code. A hook denies agent writes to the core paths, so the split is enforced, not just requested.", "BULLET_6": "An eval suite runs the whole pipeline headless against fixed tasks, trap tasks included, each with hidden checks. The results are logged per commit of the box, so a change to a rule or an agent is measured instead of guessed.", "CHALLENGE_1": "Keeping the rules short enough to stay useful. Long general guidelines just get ignored, so they are split into files per stack, per repo and per job, and each agent loads only the ones that apply to it.", "CHALLENGE_2": "Making the gates something an agent cannot talk its way past. Every check runs as a hook outside the agent: a failing build blocks the commit, a read of a secret is refused, a write to a core path in assist mode is denied, no matter what the agent believes about its own work.", "CHALLENGE_3": "A reviewer that has watched the code being written approves it. So every agent starts with an empty context and gets only the files its job needs: the reviewer sees spec, plan and diff, never the chat. The price is that each agent reads the code again, so the pipeline is slower than one long chat.", "CHALLENGE_4": "Knowing whether a change to the box helps. Every edit to a rule felt like an improvement, so I built the eval suite: fixed tasks with hidden checks, run before and after a change with the same models. The suite checks itself too: the untouched fixture must fail every task and the reference solution must pass, otherwise a green run means nothing.", "LEARNING_1": "Prompt wording mattered far less than I expected. Almost all of the difference came from the scaffolding.", "LEARNING_2": "Reviewing and gating the agent's output the same way you would a new colleague's pull request turned out to be the most useful rule of the lot.", "LEARNING_3": "Small agents with a fresh context each beat one long chat. Not because the model gets smarter, but because nobody carries earlier mistakes forward, and every hand-over is a file I can read and correct.", "DECISION_1": "Hooks instead of permission rules. Claude Code can block tools by rule, but a rule is per tool: a deny on reading .env files does not stop \"cat .env\" in the shell. So the secret guard, the commit gate and the type check are hooks that see every call. And since a hook timeout once let commits through unchecked, the gate fails closed: no answer means red.", "DECISION_2": "The host's Docker socket instead of a Docker daemon inside the container. That way the Testcontainers tests of the Java repos run in the commit gate instead of being skipped. The downside is in the README: whoever has the socket is root on the host. So the box is a work environment with pinned tools, not a sandbox.", "DECISION_3": "A second model, not only a second context. At first one model did everything and the second was never used. Now the model that plans and reviews is not the one that implements, because a second opinion is worth more when it does not come from the same model. For a one-file fix the switch is not worth it, so small changes skip steps of the pipeline.", "RESULT": "The box has been my daily tool since July 2026, in every repo I work in, at work and at home. The build with real Testcontainers runs in the commit gate. The pipeline passes every task of the eval suite, while the untouched starting point fails every one. Mostly that shows the tasks are still too easy; harder ones are the next step." }, "TBP": { "TITLE": "To Be Played", "SHORT_DESCRIPTION": "A backlog tracker for Steam and PlayStation that helps decide what to play next.", "INTRODUCTION": "My game library has far more games than I will ever finish. To Be Played syncs Steam and PlayStation, imports lists from GOG, Playnite, Grouvee and HowLongToBeat, keeps the backlog with status, rating and notes, and suggests what to play next for the time I have. Behind it are three services: an auth service that signs the login tokens, a core service for the library and the statistics, and a React frontend. All of it is self-hosted on the same server as this site.", "BULLET_1": "Steam sync of owned games, playtime, achievements and wishlist, PlayStation sync of titles, playtime and trophies, by hand and every night, plus time to beat from IGDB.", "BULLET_2": "\"What to play next\": seven rows of three suggestions, such as the next part of a series, the same studio, or games that people you follow played or finished. A time budget filters every row; the mood filter only shapes the random picks.", "BULLET_3": "Public profiles, following other players with an inbox of their progress, and Pile Points for every game played off the pile.", "BULLET_4": "German and English, light and dark mode, and it works on the phone. Auth, Core and Frontend each ship as their own Docker image.", "BULLET_5": "Over 3,400 tests across the three services, ArchUnit rules that keep the layers apart, and Testcontainers for the real database. CI runs the full build and tags every image by commit.", "CHALLENGE_1": "Login across two services. Auth signs the tokens with an Ed25519 key, and Core only holds the public key, so Core can check a login but never create one. Core fetches that key from Auth at runtime, looked up by key id, and the tokens live in HttpOnly cookies, so no script on the page can read them.", "CHALLENGE_2": "Steam's limits. Every Steam call is rate limited to two per second and retried only where a retry is safe (owned games, schema, achievements). A sync first claims its run in the database, so two syncs of the same user never run at the same time, and the nightly sync only touches users seen in the last 14 days.", "CHALLENGE_3": "Suggesting a game is harder than it sounds. Each row of suggestions needs its own rules, and the lists behind every number must use the same rules as the number, so a game counts the same way everywhere.", "CHALLENGE_4": "Login abuse. Too many failed logins get a 429 with Retry-After. An unknown email still pays for a full BCrypt check, so the timing gives nothing away. A header guard blocks CSRF, CORS allows exact origins only, and the PlayStation token is encrypted at rest.", "LEARNING_1": "Splitting an app into services only pays off with a clear contract. The token claims are pinned in a contract file that both services test against, and a script I run by hand checks that both copies match.", "LEARNING_2": "A hobby project still needs the boring parts: daily database dumps kept for seven days, four weeks and six months, migrations, and a written deploy order. They decided more about how stable it runs than any feature did.", "DECISION_1": "A key pair instead of a shared secret. The first version signed the tokens with one HMAC secret that Auth and Core both knew. Now Auth holds the private Ed25519 key and publishes the public one on a JWKS endpoint. Core can check a login but never issue one, and when a token carries an unknown key id, Core fetches the new key itself.", "DECISION_2": "Mail through an outbox table instead of sending over SMTP inline. Every mail is written as a row in mail_outbox inside the transaction of its event, and a job sends the rows every minute. So a registration never fails because of the mail server, and no mail is lost when Auth restarts. With one Auth instance, at-least-once is enough.", "DECISION_3": "Installable, but no service worker. The app has a web app manifest and installs on phone and desktop, but no service worker, so a deploy never meets an old cached bundle. The downside is that there is no offline mode.", "RESULT": "Today Auth, Core and the frontend run as their own containers next to this site, with one PostgreSQL instance with two databases, a nightly sync and daily dumps. Anyone with a confirmed e-mail can register; accounts are approved by hand. I use it every day: what I play next now comes from a list, not from scrolling through the Steam library." } }, "IMPRINT": { "TITLE": "Imprint", "PARAGRAPH": "Information pursuant to Section 5 DDG", "COUNTRY": "Germany", "CONTACT": "Contact" }, "PRIVACY": { "TITLE": "Privacy policy", "CONTROLLER": "Controller", "SECTIONS": { "HOSTING": { "TITLE": "Hosting", "TEXT": "This site runs on a virtual private server (VPS) from Hostinger International Ltd., 61 Lordou Vironos str., 6023 Larnaca, Cyprus. A data processing agreement under Art. 28 GDPR is in place with Hostinger (Hostinger Data Processing Agreement). The web server stores no access logs. Only in case of technical errors an error log may briefly contain the IP address. Legal basis: Art. 6 (1) (f) GDPR (secure operation of the site)." }, "STORAGE": { "TITLE": "Local storage", "TEXT": "The site stores only your language and theme (light or dark) in the local storage of your browser. There are no cookies, no tracking and no analytics tools." }, "SERVICE_WORKER": { "TITLE": "Service worker", "TEXT": "A service worker keeps files of this site in the browser cache, so the site loads faster and works offline. No data is sent to third parties." }, "FONTS": { "TITLE": "Fonts", "TEXT": "Fonts (Roboto, Material Icons) are loaded from this server. There is no connection to Google." }, "EXTERNAL_LINKS": { "TITLE": "External links", "TEXT": "Links to Gitea, LinkedIn and Codeberg are only opened when you click them. From then on the privacy policy of the respective provider applies." }, "RIGHTS": { "TITLE": "Your rights", "TEXT": "You have the right to access, rectification, erasure, restriction of processing, data portability and objection (Art. 15 to 21 GDPR). Contact: the e-mail address above." }, "AUTHORITY": { "TITLE": "Right to lodge a complaint", "TEXT": "You can complain to a supervisory authority. Responsible: Bayerisches Landesamt für Datenschutzaufsicht (BayLDA), Promenade 18, 91522 Ansbach." } } }, "PATHFINDING": { "TITLE": "Pathfinding Algorithms", "START_NODE": "Start Node", "END_NODE": "End Node", "WALL": "Wall", "CLEAR_NODE": "Clear", "DIJKSTRA": "Start Dijkstra", "ASTAR": "Start A*", "NORMAL_CASE": "Test Scenario", "EDGE_CASE": "A* Edge Case Scenario", "RANDOM_CASE": "Random Case", "CLEAR_BOARD": "Empty Board", "VISITED": "Visited", "PATH": "Path", "COMPARE": "Compare (A* vs Dijkstra)", "VISITED_NODES": "Visited nodes", "PATH_LENGTH": "Path length", "EXECUTION_TIME": "Execution Time", "EXPLANATION": { "TITLE": "Algorithms", "DIJKSTRA_EXPLANATION": " is guaranteed to find the shortest path if all edge costs are non-negative. Advantage: optimal and without heuristics. Disadvantage: often visits a large number of nodes (can be slower for large grids).", "ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra).", "DISCLAIMER": "This A* implementation is deliberately kept simple. It only moves in four directions and each step costs 1. The heuristic is minimal and only serves to demonstrate the principle of A* compared to Dijkstra. The goal is not an optimal or production-ready A* algorithm, but a clear visualisation of how heuristics can speed up the search." }, "ALERT": { "START_END_NODES": "Please select a start and end node before running the algorithm." }, "HOW_IT_WORKS": { "INTRO": "The grid is a two-dimensional array of nodes. Each node stores its distance from the start, its predecessor and, for A*, the heuristic h and the sum f = g + h. Both algorithms run to the end first and return the list of visited nodes and the path. The animation then replays that list node by node with setTimeout.", "ITEM_1": "Dijkstra: all nodes sit in one list. Dijkstra sorts the whole list by distance in every step (O(n² log n)) and takes the next node. A heap would be faster; with at most 22,500 nodes this is enough here. If the next node has distance infinity, the goal cannot be reached.", "ITEM_2": "A*: same structure, but only the open list is sorted, by f = g + h; on equal f the smaller h wins. The heuristic is the Manhattan distance. It is admissible because each step costs 1 and there are only four directions, so the path stays optimal.", "ITEM_3": "The path is read at the end from the predecessor chain, from the goal back to the start.", "ITEM_4": "Compare mode: start, goal and walls are copied into a second grid. A* runs on the first, Dijkstra on the second, both animations run at the same time. The number of visited nodes shows the difference.", "ITEM_5": "Execution time is the search only, without the animation. Memory: one node object per cell, at most 150 × 150 = 22,500 nodes." } }, "SORTING": { "TITLE": "Sorting Algorithms", "ALGORITHM": "Algorithm", "START": "Start Sorting", "RESET": "Reset", "GENERATE_NEW_ARRAY": "Generate New Array", "SOUND_ON": "Sound On", "SOUND_OFF": "Sound Off", "EXECUTION_TIME": "Execution Time", "ARRAY_SIZE": "Number of Bars", "EXPLANATION": { "TITLE": "Algorithms", "BUBBLE_SORT_EXPLANATION": "repeatedly compares adjacent elements and swaps them if they are in the wrong order. The largest element \"bubbles\" to the end of the list like an air bubble. Advantage: Extremely simple to understand and implement; detects already sorted lists very quickly. Disadvantage: Very inefficient for large lists (runtime O(n²)). Rarely used in practice.", "COCKTAIL_SORT_EXPLANATION": "(also Shaker Sort) is an extension of Bubble Sort. Instead of only moving from left to right, it reverses direction on each pass, alternately pushing the largest element to the right and the smallest to the left. Advantage: Faster than Bubble Sort, because small elements at the end move to the front more quickly (solving the \"turtle problem\"). Disadvantage: Stays in the O(n²) runtime class, so it remains inefficient for large data sets.", "QUICK_SORT_EXPLANATION": "follows the \"divide and conquer\" principle. A \"pivot\" element is selected, and the array is divided into two halves: elements smaller than the pivot and elements larger than the pivot. Advantage: On average one of the fastest sorting algorithms (O(n log n)); requires no additional memory (in-place). Disadvantage: Slow in the worst case (O(n²)) if the pivot is chosen poorly. Is not stable (changes order of equal elements).", "HEAP_SORT_EXPLANATION": "organizes the data initially into a special tree structure (Binary Heap). The largest element (the root) is extracted and sorted to the end, then the tree is repaired. Advantage: Guarantees a fast runtime of O(n log n), even in the worst case. Requires almost no additional memory. Disadvantage: Often slightly slower than Quick Sort in practice because the jumps in memory (heap structure) utilize the CPU cache less effectively.", "TIMSORT_EXPLANATION": "is a hybrid sorting algorithm derived from Merge Sort and Insertion Sort. It divides the array into small 'runs' and sorts each using Insertion Sort, then merges them step by step using Merge Sort. Advantage: Extremely efficient on real-world data that is often partially sorted — O(n log n) in the worst case and O(n) in the best case. It is the standard sorting algorithm in Python and Java. Disadvantage: More complex to implement than a pure algorithm and requires additional memory for the merge step.", "DISCLAIMER": "The choice of the \"best\" sorting algorithm depends heavily on the data and the constraints. In computer science, three scenarios are often considered:", "DISCLAIMER_1": "Best Case: The data is already nearly sorted (Bubble Sort shines here, for example).", "DISCLAIMER_2": "Average Case: The statistical norm.", "DISCLAIMER_3": "Worst Case: The data is arranged in the most unfavorable way possible (Quick Sort performs poorly here without optimization, while Heap Sort remains stable).", "DISCLAIMER_4": "Additionally, there is almost always a Time-Space Trade-off: Algorithms that are extremely fast (like Merge Sort) often require a lot of additional working memory. Algorithms that work directly in existing memory (like Heap Sort) save space but are sometimes more complex or slightly slower. Thus, there is no \"one-size-fits-all\" solution." }, "HOW_IT_WORKS": { "INTRO": "Each algorithm runs to the end first and stores a copy of the array (snapshot) in a list at every comparison and every swap. The animation then replays the snapshots at 100 ms per step. Execution time is the run including the copies, not the animation.", "ITEM_1": "Bubble sort and cocktail sort: O(n²) comparisons, in place. Cocktail sort changes direction every pass and stops as soon as a pass swaps nothing.", "ITEM_2": "Quick sort: Lomuto partition with the last element as pivot, recursive. Average O(n log n), worst case O(n²), for example on already sorted data.", "ITEM_3": "Heap sort: builds a max heap inside the array (heapify from the middle to the front) and swaps the root to the end. Always O(n log n), no extra memory.", "ITEM_4": "Tim sort (simplified): insertion sort on fixed blocks of 32 elements, then bottom-up merge with helper arrays. No run detection and no galloping like the original. With 200 bars that is 7 blocks and 3 merge levels.", "ITEM_5": "Memory: the snapshot list is the biggest item, O(steps · n). Bubble sort with 200 bars produces up to about 40,000 snapshots of 200 entries each." } }, "GOL": { "TITLE": "Conway's Game of Life", "START": "Start", "PAUSE": "Pause", "RANDOM_SCENE": "Random", "EMPTY_SCENE": "Empty", "SIMPLE_SCENE": "Glider", "PULSAR_SCENE": "Pulsar", "GUN_SCENE": "Gun", "ALIVE": "Alive", "DEAD": "Empty", "SPEED": "Time per Generation", "EXPLANATION": { "TITLE": "Explanation", "EXPLANATION": "The game proceeds step by step. First, an initial generation of living cells is defined on the playing field. From the current generation (the overall state of the playing field), the next generation is determined. The state of each individual cell in the next generation is derived from simple rules based on its current state and the current states of its eight neighboring cells (Moore neighborhood).", "DISCLAIMER": "According to Conway’s original rule, a cell is alive in the next round if exactly three cells are alive in its 3x3 neighborhood beforehand, counting itself only if necessary, that is:", "DISCLAIMER_1": "A living cell remains alive in the next generation if it has either two or three living neighbors.", "DISCLAIMER_2": "A dead cell is “born” (is alive in the next generation) if it has exactly three living neighbors.", "DISCLAIMER_3": "A living cell “dies” (is dead in the next generation) if it has fewer than two (underpopulation) or more than three (overpopulation) living neighbors.", "DISCLAIMER_4": "A dead cell remains dead if it does not have exactly three living neighbors." }, "HOW_IT_WORKS": { "INTRO": "The board is two grids of cells with an alive flag: a read grid and a write grid (double buffering). Each generation reads only from one and writes only to the other, then both are swapped. So a cell that already changed does not affect its neighbors in the same generation.", "ITEM_1": "For each cell the eight neighbors are counted (Moore neighborhood). The border is cut off, not wrapped: border cells have fewer neighbors, the board is not a torus.", "ITEM_2": "Time per generation is O(rows · columns · 8). At 200 × 200 that is 40,000 cells and 320,000 neighbor checks.", "ITEM_3": "The loop is an async loop: compute, draw, then wait until the configured time per generation has passed. If no cell changes any more, the game stops by itself.", "ITEM_4": "The whole grid is drawn onto a canvas: one fill operation for the background plus one per living cell." } }, "LABYRINTH": { "TITLE": "Labyrinth Generation", "PRIM": "Generate Prim's Labyrinth", "KRUSKAL": "Generate Kruskal's Labyrinth", "EXPLANATION": { "TITLE": "Algorithms", "PRIM_EXPLANATION": "starts at a random point and expands the labyrinth by always selecting a random neighboring wall of an already visited cell and opening it. Advantage: Produces very uniform, natural-looking labyrinths with many short dead ends. Visually, it appears like organic growth from a central point.", "KRUSKAL_EXPLANATION": "considers all walls of the grid as potential paths. It randomly selects walls and opens them only if the two adjacent cells are not yet connected (preventing cycles). Advantage: Produces a very complex labyrinth with many long, winding paths. Visually, it is engaging because the labyrinth emerges simultaneously in many places and eventually merges into a whole.", "DISCLAIMER": "Both algorithms are based on the principle of the 'Minimum Spanning Tree'. This means for your labyrinth:", "DISCLAIMER_1": "Perfect labyrinth: There are no closed loops – every point is reachable, but there is always exactly one path between any two points.", "DISCLAIMER_2": "Reachability: Since it is a spanning tree, every cell in the grid is guaranteed to be part of the labyrinth; there are no isolated areas.", "DISCLAIMER_3": "Randomness: By weighting the edges with random values, each run produces completely new, unique structures.", "DISCLAIMER_4": "Application: Such labyrinths are the perfect test environment for pathfinding algorithms such as Dijkstra or A*." }, "HOW_IT_WORKS": { "INTRO": "The grid has 101 × 101 cells. Cells with even coordinates are rooms (51 × 51 = 2,601), the cells between them are walls. Both algorithms first compute the order in which walls are opened; the animation then opens them one by one. The result is a spanning tree: every room is reachable and there is exactly one way between two rooms.", "ITEM_1": "Prim (randomized): starts in a random room and keeps a frontier list of walls to rooms not yet visited. Each step picks a random wall (swap with the last element, then pop: O(1)) and opens it if the room behind it is still unvisited. Randomness replaces edge weights.", "ITEM_2": "Kruskal (randomized): all 5,100 walls between two rooms are collected and shuffled with Fisher-Yates. Each room carries a set id. A wall is opened when the rooms have different ids; then all rooms of one set get the id of the other. That is one pass over the grid per union, simpler than union-find and fast enough for 2,601 rooms.", "ITEM_3": "Start and goal sit in two random corners. Dijkstra and A* then run on the same grid as on the pathfinding page.", "ITEM_4": "Memory: one node object per cell (10,201) plus the list of cells to open." } }, "FRACTAL": { "TITLE": "Fractals", "ALGORITHM": "Algorithms", "RESET": "Reset", "COLOR_SCHEME": "Color Scheme", "MAX_ITERATION": "Max. Resolution", "EXPLANATION": { "TITLE": "Mathematical Art", "MANDELBROT_EXPLANATION": "is based on the iterative formula 'z_{n+1} = z_n^2 + c'. It checks for every point in the complex plane whether the sequence remains stable or escapes to infinity. Advantage: Known as the 'Apple Man', it is the mother of all fractals, offering infinite variety and self-similar structures to zoom into forever.", "JULIA_EXPLANATION": "uses the same formula as Mandelbrot but fixes the parameter 'c' and varies the starting value. Depending on the choice of 'c', it creates delicate, cloud-like structures or disconnected 'dust'. Advantage: Allows for immense aesthetic variance, as every coordinate in the Mandelbrot set produces its own unique Julia fractal.", "NEWTON_EXPLANATION": "is created by visualizing Newton's method for finding roots of a complex function. Each pixel is colored based on which root the algorithm converges to. Advantage: Produces fascinating star-shaped symmetries and complex boundaries where the attraction basins of the roots meet in a chaotic dance.", "BURNING_SHIP_EXPLANATION": "is a variation of the Mandelbrot set where the absolute values of the real and imaginary parts are taken before each iteration: '(|Re(z)| + i|Im(z)|)^2 + c'. Advantage: Generates a striking, asymmetrical structure resembling a ship on fire. It feels more 'mechanical' and darker compared to the classical sets.", "DISCLAIMER": "All these fractals are based on the principle of iteration and the butterfly effect. This means for your visualization:", "DISCLAIMER_1": "Infinite Depth: No matter how far you zoom in, new complex structures appear that often resemble the whole (self-similarity).", "DISCLAIMER_2": "Escape-Time Algorithm: Colors usually represent how quickly a sequence exceeds a certain threshold—the faster it escapes, the 'hotter' or brighter the color.", "DISCLAIMER_3": "Complex Numbers: Calculations don't happen in a standard coordinate system, but in the complex plane using real and imaginary components.", "DISCLAIMER_4": "Computational Load: Since hundreds of calculations are performed for every single pixel, fractals are a classic benchmark for GPU and processor performance.", "DISCLAIMER_BOTTOM": "Graphics cards calculate with 32-bit floating point numbers (float) by default. These only have about 7 digits of accuracy. At very high zoom levels (> 100.000), the difference between two pixels is so tiny that the graphics card can no longer display it. It calculates exactly the same value for 10 pixels next to each other -> you see blocks or stair steps." }, "HOW_IT_WORKS": { "INTRO": "The image is a fragment shader (GLSL) on a full-screen quad in Babylon.js. Each pixel computes its own sequence: for Mandelbrot, Julia and Burning Ship z → z² + c until |z|² > 4 or the maximum iteration count is reached (escape-time algorithm). Newton iterates z → z − (z³ − 1) / (3z²) and colors by the root the pixel converges to.", "ITEM_1": "The CPU sends only uniforms per frame: center, zoom, maximum iterations, algorithm, color scheme and the Julia constant. The GPU computes all pixels in parallel; cost O(pixels · iterations).", "ITEM_2": "The maximum iteration count grows with the zoom: 100 + 200 · log10(zoom), so up to about 1,060 at the maximum zoom of 64,000.", "ITEM_3": "Zooming with the mouse wheel keeps the point under the cursor fixed: its world point is set equal before and after the zoom and the center is moved accordingly.", "ITEM_4": "The color comes from the ratio iterations / maximum; points that never escape get the color for t ≈ 1 (dark red in the Blue-Gold scheme).", "ITEM_5": "Under WebGPU Babylon.js translates the same GLSL shader to WGSL; there is no separate compute path." } }, "FRACTAL3D": { "TITLE": "3D Fractals", "ALGORITHM": "Algorithms", "MANDELBULB": "Mandelbulb", "MANDELBOX": "Mandelbox", "JULIA": "Julia", "EXPLANATION": { "TITLE": "3D Fractal Worlds", "MANDELBULB_EXPLANATION": "is considered the 'Holy Grail' of 3D fractals. Since complex numbers are only two-dimensional, this fractal uses spherical coordinates and high powers (usually v^8 + c) to project the Mandelbrot set into 3D space. Benefit: Creates an organic, extremely detailed structure reminiscent of plants, coral reefs, or alien landscapes.", "MANDELBOX_EXPLANATION": "is based not on smooth curves, but on geometric 'folding' and scaling (Box-Folding & Sphere-Folding). Space is repeatedly folded and mirrored like origami. Benefit: Produces strictly geometric, mechanical-looking structures that resemble endless futuristic cities, the Borg cube, or complex sci-fi architecture.", "JULIA_EXPLANATION": "is the 3D counterpart to the 2D Julia set. While the Mandelbulb is a 'map' of all fractals, here we fix the parameter 'c' and vary the starting point and it changes over time. Benefit: Unlike the solid Mandelbulb, Julia Bulbs are often hollow, forming complex tunnel systems or bubble-like structures perfect for flying through with the camera.", "DISCLAIMER": "This visualization uses a technique called 'Raymarching' (Sphere Tracing). This means:", "DISCLAIMER_1": "No Polygons: There are no triangles or meshes. The shape is calculated mathematically for every pixel in real-time.", "DISCLAIMER_2": "Distance Estimation: The algorithm 'marches' light rays forward by calculating the safe distance to the nearest object without hitting it immediately.", "DISCLAIMER_3": "Infinite Detail: Since the surface is mathematically defined, it never pixelates when zooming in – new structures always emerge.", "DISCLAIMER_4": "Light: depth comes from the surface normal: faces that point at the camera are brighter, plus a fixed ambient term. Shadows and ambient occlusion are not computed." }, "HOW_IT_WORKS": { "INTRO": "There is no geometry. A fragment shader sends one ray per pixel from the camera into the scene and walks it with sphere tracing (raymarching): a distance estimator says how far the surface is at least, and the ray jumps by that amount (times 0.8 as a safety factor), at most 128 steps. Before that, a sphere test (radius 6) limits the ray to the relevant range.", "ITEM_1": "Mandelbulb: 8 iterations in spherical coordinates with power 8 (z → z⁸ + c). The distance is 0.5 · ln(r) · r / dr, where dr is the running derivative.", "ITEM_2": "Mandelbox: 15 iterations of box fold (mirror at ±1), sphere fold (scale by radius) and scale 2.8.", "ITEM_3": "Julia bulb: like Mandelbulb, but with a fixed constant c that moves slightly over time, so the shape moves.", "ITEM_4": "Normals come from four extra distance queries. Lighting: light from the camera direction plus 40 % ambient, no shadows, no ambient occlusion. Color: orbit trap (smallest radius during iteration) mapped to a cosine palette.", "ITEM_5": "Cost per frame: pixels × ray steps × iterations. At full HD with the Mandelbox that is about 4 billion iteration steps in the worst case. It runs as a fragment shader on the GPU only; there is no CPU path." } }, "PENDULUM": { "TITLE": "Double pendulum", "TRAIL_DECAY_TIME": "Trail length", "DAMPING": "Damping", "ATTRACTION": "Attraction", "L1_LENGTH": "Length L1", "L2_LENGTH": "Length L2", "M1_MASS": "Mass M1", "M2_MASS": "Masse M2", "POKE_M1": "Poke M1", "POKE_M2": "Poke M2", "RESET": "Reset", "EXPLANATION": { "TITLE": "Chaos Theory: The Double Pendulum", "EXPLANATION": "The double pendulum is one of physics' most famous and fascinating examples of a dynamic system that generates 'deterministic chaos'. It simply consists of a standard pendulum with a second pendulum attached to its lower end. Although the underlying laws of classical mechanics are strictly mathematically defined, the long-term behavior of the double pendulum is absolutely unpredictable. In physics, it is considered the classic showcase object for the so-called butterfly effect.", "DISCLAIMER": "The simulation computes the pendulum's equations of motion once per rendered frame with a fixed time step dt = 0.015, on the GPU (WebGPU) or on the CPU as fallback. The following applies:", "DISCLAIMER_1": "Extreme Sensitivity: The tiniest changes in the initial conditions (e.g., a thousandth of a degree deviation in the starting angle or mass) lead to a completely different, chaotic trajectory after just a short time.", "DISCLAIMER_2": "Deterministic Chaos: The movement may look completely wild and random, but it isn't. If you restart the simulation with the exact same values, the pendulum will follow 100% the same path.", "DISCLAIMER_3": "Numerical Integration: Since computers do not calculate time continuously but in tiny steps (dt), minute mathematical rounding errors occur in every frame. These add up over time and further influence the chaos.", "DISCLAIMER_4": "Energy Conservation & Friction: In a perfect physical system without resistance, the pendulum would swing forever. For a natural look, the algorithm uses an artificial damping factor that simulates air friction and eventually brings the system to a halt.", "DISCLAIMER_BOTTOM": "NOTE: If too many impulses are fed into the system, the simulation becomes unstable. The pendulum will then just hang down and the simulation will have to be restarted." }, "HOW_IT_WORKS": { "INTRO": "The motion follows the equations of motion of the double pendulum (Lagrangian form, formulas as in the Wikipedia article). Each rendered frame computes one step with a fixed time step dt = 0.015: first the angular accelerations a1 and a2, then the angular velocities (times damping, plus impulse), then the angles (semi-implicit Euler).", "ITEM_1": "WebGPU: a compute shader with a single thread computes the physics step; the state (θ1, θ2, v1, v2) stays in a storage buffer on the GPU. A second compute shader (64 threads per workgroup, one per pixel) draws rods, masses and trail directly into a pixel buffer; the trail fades every frame by the trail length factor. A fragment shader displays the buffer.", "ITEM_2": "Without WebGPU: the same physics in TypeScript on the CPU. The trail comes from ping-pong between two render target textures: a GLSL shader reads the old image, fades it and draws the two masses again; a second shader displays trail and rods.", "ITEM_3": "Pixel buffer encoding: intensity 0 to 1 for trail 1, +2 for trail 2, +10 and +20 for the rods. So one float per pixel is enough.", "ITEM_4": "The time step is fixed and not tied to the frame rate: at 120 Hz the pendulum runs twice as fast as at 60 Hz. Euler does not conserve energy exactly; big impulses can make the simulation unstable (see note)." } }, "CLOTH": { "TITLE": "Cloth simulation", "WIND_ON": "Wind On", "WIND_OFF": "Wind Off", "OUTLINE_ON": "Show Mesh", "OUTLINE_OFF": "Hide Mesh", "STIFFNESS": "Stiffness", "ELONGATION": "Elongation", "RESTART_SIMULATION": "Restart Simulation", "EXPLANATION": { "TITLE": "Real-time Cloth Simulation on the GPU", "CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation", "XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)", "GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelization", "DATA_STRUCTURES_EXPLANATION_TITLE": "Data Structures", "CLOTH_SIMULATION_EXPLANATION": "Cloth simulations usually model textiles as a grid of mass points (vertices) held together by invisible connections. The goal is to represent physical influences like gravity, wind, and collisions in real time without the material tearing or stretching unnaturally like rubber.", "XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) is a modern algorithm that manipulates point positions directly to satisfy distance conditions (constraints) instead of calculating accelerations. The 'Extended' means that true physical stiffness is simulated independently of the framerate. Advantage: Absolutely stable, does not explode, and topological changes (like cutting cloth) are trivial. Disadvantage: It is an iterative approximation method and slightly less physically accurate than complex matrix solvers.", "GPU_PARALLELIZATION_EXPLANATION": "To calculate tens of thousands of points in parallel on the graphics card, one must prevent 'race conditions' – i.e., two processing cores shifting the same node at the exact same time. The solution is called 'Independent Sets' (or Graph Coloring): The connections are divided into isolated groups (here 8 phases: 2 horizontal, 2 vertical, 4 diagonal) in which not a single point overlaps. This allows the GPU to process each group blindly and at maximum speed.", "DATA_STRUCTURES_EXPLANATION": "For maximum GPU performance, data must be memory-aligned (16-byte alignment). Instead of using many individual variables, information is cleverly packed into blocks of four (vec4). A vertex stores, for example, [X, Y, Z, Inverse_Mass]. If a point has an inverse mass of 0.0, the algorithm ignores it, and it floats motionlessly in the air – an elegant trick for pinning cloth without extra if/then statements.", "DISCLAIMER": "XPBD vs. Mass-Spring Systems: In physical simulations, there are fundamental architectural differences when solving equations:", "DISCLAIMER_1": "Classical Mass-Spring Systems: Here, forces (Hooke's Law) are calculated, leading to accelerations and ultimately new positions. There are two ways to mathematically project these into the future (integration):", "DISCLAIMER_2": "Explicit Solvers (e.g., Forward Euler): These rigidly calculate the next step solely from the current state. They are easy to program but extremely unstable for stiff cloths. Forces can escalate and the simulation 'explodes' unless tiny, very performance-heavy time steps are chosen.", "DISCLAIMER_3": "Implicit Solvers (e.g., Backward Euler): These calculate the next step based on the future state. This is mathematically highly stable but requires solving massive global matrix equation systems in every frame. This is harder to parallelize on the GPU and breaks down if the structure changes (e.g., when the cloth is cut).", "DISCLAIMER_4": "The XPBD Compromise: XPBD completely bypasses this complex matrix problem by acting as a local solver. It combines the absolute stability of an implicit solver with the enormous speed, parallelizability, and dynamic adaptability of an explicit system." }, "HOW_IT_WORKS": { "INTRO": "The cloth is a grid of 100 × 100 = 10,000 particles. Each particle is a vec4 [x, y, z, 1/mass]; the top row has inverse mass 0 and is pinned. 39,402 distance constraints hold the particles together: horizontal, vertical and two diagonals against shearing. Per frame: integration (gravity, wind, position prediction), then 15 substeps in which every constraint is solved once (XPBD), finally the velocity from the position change.", "ITEM_1": "XPBD: per constraint the error C = |pA − pB| − rest length is computed and both particles are moved by λ = −C / (wA + wB + α) along the connection, weighted by inverse mass. α = compliance / dt² makes the stiffness independent of the time step. The elongation slider scales the rest length.", "ITEM_2": "GPU (WebGPU): three compute shaders (integrate, solve, velocity) with 64 threads per workgroup. All data stays in storage buffers on the GPU; the vertex shader reads the positions straight from the buffer. Nothing goes back to the CPU.", "ITEM_3": "Graph coloring: two threads must never write the same particle. So the constraints are sorted into 8 phases (even and odd columns, even and odd rows, two per diagonal). Within one phase no particle has two constraints, so each phase runs without locks. That is 15 · 8 = 120 solver dispatches per frame (plus one each for integrate and velocity).", "ITEM_4": "Without WebGPU: the same three steps in TypeScript on the CPU with the identical memory layout (Float32Array). The positions are copied into the mesh's vertex buffer every frame. 39,402 · 15 = 591,030 constraint solves per frame is a lot for a CPU; the frame rate drops clearly.", "ITEM_5": "Wind: fixed direction (5, 0, 15) plus a sine flutter from position and time. The flutter part also adds a slight lift (y: flutter · 2)." } }, "GPU": { "WEBGL_FALLBACK": "WebGPU is not available. Using WebGL as fallback. Performance may be reduced.", "NOT_SUPPORTED": "Neither WebGPU nor WebGL could be initialized. GPU visualizations are unavailable." }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": { "TITLE": "Pathfinding", "DESCRIPTION": "Dijkstra and A* on a grid, alone or side by side." }, "SORTING": { "TITLE": "Sorting", "DESCRIPTION": "Visualizing various sorting algorithms." }, "GOL": { "TITLE": "Conway's Game of Life", "DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970." }, "LABYRINTH": { "TITLE": "Maze Generation", "DESCRIPTION": "Maze generation with Prim and Kruskal, then pathfinding inside it." }, "FOUR_COLOR": { "TITLE": "Four Color Theorem", "DESCRIPTION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color." }, "FOURIER": { "TITLE": "Fourier Epicycles", "DESCRIPTION": "Any closed shape is drawn by a chain of rotating circles. Draw your own shape and watch the circles rebuild it with a discrete Fourier transform." }, "FRACTAL": { "TITLE": "Fractals", "DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)." }, "FRACTAL3D": { "TITLE": "Fractals 3D", "DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)." }, "PENDULUM": { "TITLE": "Double pendulum", "DESCRIPTION": "Chaotic double pendulum, physics in a WebGPU compute shader (fallback WebGL + CPU)." }, "CLOTH": { "TITLE": "Cloth simulation", "DESCRIPTION": "Cloth simulation with XPBD on the GPU (WebGPU, fallback WebGL + CPU)." }, "NOTE": "Note", "GRID_HEIGHT": "Height", "GRID_WIDTH": "Width", "HOW_IT_WORKS": "How it works", "SOURCE": "Source code" }, "FOUR_COLOR": { "TITLE": "Four Color Theorem", "GENERATE": "Generate Map", "SOLVE": "Auto Solve", "CLEAR": "Clear Colors", "COLOR_1": "Color 1", "COLOR_2": "Color 2", "COLOR_3": "Color 3", "COLOR_4": "Color 4", "EXECUTION_TIME": "Execution Time", "STATUS": { "LABEL": "Status", "INCOMPLETE": "Map is not fully colored yet.", "SOLVED": "Congratulations! You solved the map correctly!", "CONFLICTS": "Warning: Adjacent regions have the same color!", "INVALID": "Map is fully colored, but contains conflicts." }, "EXPLANATION": { "TITLE": "Four Color Theorem", "EXPLANATION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color.", "DISCLAIMER": "This algorithm uses backtracking to find a valid coloring for the generated regions.", "DISCLAIMER_1": "Map Generation: Regions are generated using a random seed growth algorithm (Voronoi-like) on a grid.", "DISCLAIMER_2": "Adjacency: Two regions are considered neighbors if they share at least one edge in the grid.", "DISCLAIMER_3": "Backtracking: The solver tries colors 1-4 for each region, backtracking when a conflict is found.", "DISCLAIMER_4": "Interactive: You can also click on regions to cycle through colors manually.", "DISCLAIMER_BOTTOM": "Click to color the region. Click again to change the color." }, "ALERT": { "NO_SOLUTION": "No solution found (this should not happen for a planar map!)." }, "HOW_IT_WORKS": { "INTRO": "The map is built on a grid: one seed per 30 cells is placed at a random free position. All seeds grow at the same time by breadth-first search (one shared queue) until every cell belongs to a region. Two regions are neighbors when two of their cells share an edge.", "ITEM_1": "Data structure: a list of regions, each with a set of its neighbor ids (adjacency list). The grid stores only region id and color per cell.", "ITEM_2": "Solving: backtracking in region id order. For each region the colors 1 to 4 are tried; if none fits, the solver steps back one region. No heuristic like 'most neighbors first', so on big maps the color attempts are sometimes far above the number of regions.", "ITEM_3": "The default map (25 × 25, 20 regions) solves in under 1 ms. Large maps can take very long, because the backtracking has no heuristic and the worst case is exponential.", "ITEM_4": "Checking: walk all regions and compare each color with its neighbors, O(regions + borders)." } }, "FOURIER": { "TITLE": "Fourier Epicycles", "DRAW_HINT": "Draw a closed shape on the canvas or pick a preset", "PRESET_HEART": "Heart", "PRESET_STAR": "Star", "PRESET_SQUARE": "Square", "PRESET_DOG": "Dog", "CIRCLES": "Circles", "SPEED": "Speed", "SHOW_CIRCLES": "Show circles", "HIDE_CIRCLES": "Hide circles", "EXPLANATION": { "TITLE": "Fourier Series", "EXPLANATION": "A Fourier series says that any periodic curve is a sum of rotating circles. Each circle has its own radius, speed and start angle.", "DISCLAIMER": "This demo does it in three steps.", "DISCLAIMER_1": "Your shape is resampled to 256 equally spaced points.", "DISCLAIMER_2": "Each point is a complex number x + iy. A direct discrete Fourier transform gives one circle per frequency.", "DISCLAIMER_3": "The circles are sorted by radius. The slider keeps only the biggest N circles.", "DISCLAIMER_BOTTOM": "Fewer circles give a smoother approximation. With all circles the curve passes exactly through all 256 points." }, "HOW_IT_WORKS": { "INTRO": "The drawn shape is treated as a closed polygon and resampled to 256 points with equal arc-length spacing. Each point is a complex number x + i·y. A direct discrete Fourier transform (two nested loops, O(N²) = 65,536 steps) gives one circle per frequency with radius, start angle and rotation speed. The circles are sorted by radius.", "ITEM_1": "Frequencies run from 0 to N−1; everything above N/2 counts as a negative frequency so circles can also turn backwards.", "ITEM_2": "Each frame adds up the circle centers (O(circles)); the tip of the last circle goes onto the trace. After one full period the trace is cleared.", "ITEM_3": "Drawing uses Canvas 2D with requestAnimationFrame outside the Angular zone. dt is the time between two frames, so the speed does not depend on the frame rate.", "ITEM_4": "Presets: heart and star are formulas, the square is four corner points, the dog is a point list smoothed with three Chaikin passes (corner cutting).", "ITEM_5": "Fewer circles act like a low-pass filter: only the largest radii stay, the shape gets smoother." } }, "STOPWATCH": { "TITLE": "Stopwatch", "START": "Start", "PAUSE": "Pause", "STOP": "Stop", "RESET": "Reset", "INTERVAL_ENABLED": "Beep at interval", "FIRST_INTERVAL": "First interval", "INTERVAL_MINUTES": "Minutes", "INTERVAL_SECONDS": "Seconds", "SECOND_INTERVAL_ENABLED": "Add a second, alternating interval", "INTERVAL_HINT": "Plays a short tone every time an interval elapses. With a second interval enabled both alternate, so the second one starts right after the first.", "INTERVAL_SCHEDULE": "Next beeps at:" }, "NOT_FOUND": { "TITLE": "Page not found", "MESSAGE": "The page you are looking for does not exist or has been moved.", "BACK_HOME": "Back to start" }, "SEO": { "DEFAULT": { "TITLE": "Andreas Dahm – Playground", "DESCRIPTION": "Portfolio and interactive playground of Andreas Dahm, senior software developer: algorithm visualizations, GPU simulations and personal projects." }, "ABOUT": { "TITLE": "About me – Andreas Dahm", "DESCRIPTION": "Andreas Dahm, senior software developer and architect from Munich: experience, skills and projects in full-stack development, 3D simulation, algorithms and AI-assisted engineering." }, "PROJECTS": { "TITLE": "Projects – Andreas Dahm", "DESCRIPTION": "Selected projects by Andreas Dahm: a Steam game release, self-hosted infrastructure, game jams and a scientific diploma thesis." }, "ALGORITHMS": { "TITLE": "Algorithms – Andreas Dahm", "DESCRIPTION": "Interactive visualizations of algorithms and simulations: pathfinding, sorting, fractals, cloth and pendulum physics, running in the browser." }, "FOURIER": { "TITLE": "Fourier Epicycles – Andreas Dahm", "DESCRIPTION": "Interactive Fourier epicycles: draw a closed shape and watch a chain of rotating circles trace it with a discrete Fourier transform." }, "IMPRINT": { "TITLE": "Imprint – Andreas Dahm", "DESCRIPTION": "Legal information and contact details for the Andreas Dahm Playground website." }, "PRIVACY": { "TITLE": "Privacy policy – Andreas Dahm", "DESCRIPTION": "Privacy policy for the Andreas Dahm Playground website." }, "STOPWATCH": { "TITLE": "Stopwatch – Andreas Dahm", "DESCRIPTION": "A simple online stopwatch with configurable interval beeps." }, "NOT_FOUND": { "TITLE": "Page not found – Andreas Dahm", "DESCRIPTION": "The requested page could not be found." } } }