Jekyll2023-04-23T21:43:49-07:00https://jackpal.github.io/feed.xmlGrammerJackEssays and epigrams.Jack PalevichiA Presenter Micro-Review2023-04-21T14:00:00-07:002023-04-21T14:00:00-07:00https://jackpal.github.io/2023/04/21/IA_Presenter<p>I just finished beta testing the <a href="https://ia.net/presenter">iA Presenter</a> Markdown-based presentation software.</p>
<p>tl/dr: It’s really good! But it’s not for me.</p>
<p>What it is: A macOS app that enables you to quickly create
great looking slideshow presentations using a slightly
enhanced version of Markdown.</p>
<p>Why I liked it:</p>
<ul>
<li>Delightfully easy to use</li>
<li>Gorgeous presentation defaults</li>
<li>Opinionated tutorial, teaches you to make better presentations</li>
</ul>
<p>Why it’s not for me:</p>
<ul>
<li>My workplace uses <a href="https://www.google.com/slides/about/">Google Slides</a>.</li>
<li>I don’t create presentations except for work.</li>
<li>While iA Presenter can be used to create Markdown-based blog posts,
it’s priced for professional users, which makes it too
expensive for hobby use.</li>
</ul>
<p>Anyway, I had fun using it, and I wish iA well in
their launch and in their future endeavors.</p>Jack PalevichI just finished beta testing the iA Presenter Markdown-based presentation software. tl/dr: It’s really good! But it’s not for me. What it is: A macOS app that enables you to quickly create great looking slideshow presentations using a slightly enhanced version of Markdown. Why I liked it: Delightfully easy to use Gorgeous presentation defaults Opinionated tutorial, teaches you to make better presentations Why it’s not for me: My workplace uses Google Slides. I don’t create presentations except for work. While iA Presenter can be used to create Markdown-based blog posts, it’s priced for professional users, which makes it too expensive for hobby use. Anyway, I had fun using it, and I wish iA well in their launch and in their future endeavors.The HD 4chan Browser2023-03-29T06:48:57-07:002023-03-29T06:48:57-07:00https://jackpal.github.io/2023/03/29/HD_4chan_browser<p>I wrote <a href="https://github.com/jackpal/HD">HD</a>, a small SwiftUI app to browse the
4chan image board on an iPhone or iPad.</p>
<p>I’m proud of how nice the app is to use, and how fast it displays images, animations and videos.</p>
<!--more-->
<p>Apple doesn’t allow 4chan apps in the App Store, so for now, the only way to use HD is to
build it yourself.</p>
<h2 id="implementation-details">Implementation details</h2>
<p>The app source is small: only about 1000 lines of code. It makes extensive use of
SwiftUI and open source Swift Packages.</p>
<p>The app requires Xcode 14 and iOS 16.0 / iPadOS 16.0.</p>
<p>I used <a href="https://apps.apple.com/us/app/draw-things-ai-generation/id6444050820">Draw Things</a> to create the app
icon. Pretty good for “programmer art”.</p>
<p>Swift Packages are still a little rough to use. For example, I had to fork
the vlckit-spm package just to change
its version number to something compatible with Xcode:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/jackpal/FourChanAPI">FourChan api</a></td>
<td>4Chan content API.</td>
</tr>
<tr>
<td><a href="https://github.com/jackpal/HTMLString">HTMLString</a></td>
<td>Convert HTML content to AttributedString and/or String</td>
</tr>
<tr>
<td><a href="https://github.com/siteline/SwiftUI-Introspect">Introspect</a></td>
<td>Access the UIKit views that implement SwiftUI views.</td>
</tr>
<tr>
<td><a href="https://github.com/kean/Nuke">Nuke</a></td>
<td>Fast asynchronous image loader.</td>
</tr>
<tr>
<td><a href="https://github.com/scinfu/SwiftSoup">SwiftSoup</a></td>
<td>HTML parser.</td>
</tr>
<tr>
<td><a href="https://github.com/kirualex/SwiftyGif">SwiftyGIF</a></td>
<td>GIF image loader.</td>
</tr>
<tr>
<td><a href="https://github.com/tylerjonesio/vlckit-spm">vlckit-spm</a></td>
<td>VLC webm player.</td>
</tr>
</tbody>
</table>
<h2 id="disclaimer">Disclaimer</h2>
<p><a href="https://4chan.org">4chan.org</a> is an image board for fans of Japanese culture.</p>
<p>For historical reasons, 4chan allows a much wider range of content than most people are
comfortable with. I do not condone any of the posts or actions of any users who post on
4chan.</p>
<p>The HD App is not affiliated with or approved by the 4chan.org web site.</p>Jack PalevichI wrote HD, a small SwiftUI app to browse the 4chan image board on an iPhone or iPad. I’m proud of how nice the app is to use, and how fast it displays images, animations and videos.Advent Calendar2022-12-11T05:48:57-08:002022-12-11T05:48:57-08:00https://jackpal.github.io/2022/12/11/Advent_Calendar<p>Let’s create an iPhone app that displays an advent calendar Lock Screen widget.</p>
<!--more-->
<h1 id="create-a-new-xcode-project">Create a new Xcode project</h1>
<p>You’ll need Xcode 14.0 or later for this project.</p>
<p>Choose “File > New Project”, or click on the “Create a new Xcode Project” button.</p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Welcome_to_Xcode.png" alt="Welcome to Xcode" /></p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Create_App.png" alt="Create App" /></p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Project_settings.png" alt="Project settings" /></p>
<h1 id="add-a-widget-target">Add a widget target</h1>
<h3 id="file--add-target">File > Add Target</h3>
<p>Use the “File > Add Target” menu item to add a “Widget target”.</p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Add_Widget_Target.png" alt="Add Widget Target" /></p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Widget_Target_Options.png" alt="Widget Target Options" />
You don’t need a Live Activity. You might want a Configuration intent.</p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Activate_Widget_Scheme.png" alt="Activate Widget Scheme" /></p>
<h1 id="change-the-preview-widget-family-to-accessoryrectangular">Change the preview widget family to .accessoryRectangular</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Preview_family.png" alt="Preview family" /></p>
<h1 id="draw-the-widget">Draw the widget</h1>
<p>Lock Screen widgets render in xxx mode. This limits you to using alpha and transparency.</p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Draw_Christmas.png" alt="Draw Christmas" /></p>
<h1 id="set-the-metadata">Set the Metadata</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Set_Widget_Metadat.png" alt="Set Widget Metadat" /></p>
<h1 id="simulate">Simulate</h1>
<h3 id="-r-to-build-and-run">⌘-R to build and run</h3>
<p>Command-R to start running the app on the simulator.</p>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Running_app.png" alt="Running app" /></p>
<h1 id="lock-the-screen">Lock the screen</h1>
<h3 id="-l-or-device--lock-screen">⌘-L or Device > Lock Screen</h3>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Lock_screen.png" alt="Lock screen" /></p>
<h1 id="long-press-to-customize">Long press to customize</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Customize_step_1.png" alt="Customize step 1" /></p>
<h1 id="choose-lock-screen">Choose Lock Screen</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Customize_step_2.png" alt="Customize step 2" /></p>
<h1 id="tap-add-widgets">Tap Add Widgets</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Customize_step_3.png" alt="Customize step 3" /></p>
<h1 id="choose-advent-calendar">Choose Advent Calendar</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Add_Widgets.png" alt="Add Widgets" /></p>
<h1 id="choose-the-shape">Choose the Shape</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Choose_widget_size.png" alt="Choose widget size" /></p>
<h1 id="final-result">Final result</h1>
<p><img src="/assets/posts/2022-12-11-Advent_Calendar/Final.png" alt="Final" /></p>
<h1 id="next-steps">Next steps</h1>
<h1 id="improve-graphics">Improve graphics</h1>
<h3 id="timeline-only-needs-to-update-once-a-year-on-christmas">Timeline only needs to update once a year, on Christmas</h3>Jack PalevichLet’s create an iPhone app that displays an advent calendar Lock Screen widget.CS admissions, Fall 20222022-06-05T11:17:00-07:002022-06-05T11:17:00-07:00https://jackpal.github.io/2022/06/05/cs-admissions-fall-2022<p>tl/dr advice to kids and parents aiming for admission to a good undergraduate CS program in 2022-2023:</p>
<ul>
<li>Research what your target schools are looking for.</li>
<li>Calculate the cost/benefit ratio.</li>
<li>Have a back-up plan.</li>
</ul>
<p>Having just helped my three kids get into undergraduate CS programs at, respectively a top-10, a top-25, and a top-35 school, I want to share my family’s experience.</p>
<!--more-->
<h1 id="a-natural-experiment">A Natural Experiment</h1>
<p>A natural experiment is an unplanned experiment that happens by accident, due to natural factors. I have three kids that are spaced closely together in age. They have roughly the same abilities, schooling and so on. They all applied to US-based undergraduate CS departments in the 2020-2022 period.</p>
<p>Looking at their varying results, I can draw some conclusions. In my opinion, the biggest factors affecting which schools they got accepted to were, in order of most important to least important:</p>
<ol>
<li>What year they applied. (2022 was rougher than 2020)</li>
<li>High School GPA.</li>
<li>Quality of application essay.</li>
<li>Extra-curricular activities.</li>
</ol>
<p>One confounding factor was the decision by many colleges to stop considering SAT scores. My oldest child was able to compensate for a poorer GPA by showing excellent SAT scores. This wasn’t possible for my younger kids.</p>
<h1 id="the-application-process">The application process</h1>
<h2 id="advice-in-context">Advice in context</h2>
<p>We were lucky that our family is well-enough-off that:</p>
<ul>
<li>We could afford to apply to as many schools as we wanted.</li>
<li>My kids had time to complete as many applications as they wanted.</li>
<li>We could afford to consider attending out-of-state schools.</li>
</ul>
<p>If your situation is different, keep that in mind. (But also note that many schools offer financial assistance.)</p>
<h2 id="things-we-did-that-were-helpful">Things we did that were helpful</h2>
<ul>
<li>Develop a list of target schools.</li>
<li>Apply to a variety (academic quality and location) of schools.</li>
<li>Have safety schools.</li>
<li>Use school-specific subreddits to learn about schools.</li>
<li>Reach out to friends and relatives to get tips for applying.</li>
</ul>
<h2 id="things-we-did-that-were-not-helpful">Things we did that were not helpful</h2>
<ul>
<li>Advice from earlier, easier application cycles.
<ul>
<li>Reading old advice left us under-prepared for the difficulty of this application cycle.</li>
<li>We were shocked at the schools that rejected us.</li>
</ul>
</li>
<li>Wait until the last moment to submit applications.
<ul>
<li>Three or four times this year were spent franticly editing applications at the last minute.</li>
<li>One frustrating event was missing an application deadline due to an East Coast school’s definition of “midnight” being different than our local time zone. Luckily, it turned out that the school hadn’t intended to cut off applications that early, and so we were able to submit the next day when applications were reopened.</li>
</ul>
</li>
</ul>
<h2 id="things-we-should-have-done-but-didnt">Things we should have done, but didn’t</h2>
<ul>
<li>Apply to <em>all</em> the UC schools, not just the most desirable ones.
<ul>
<li>The UC system lets you apply to multiple schools with the same application.</li>
<li>There is no drawback to applying to all of them.</li>
</ul>
</li>
<li>
<p>Start the admissions process early.</p>
</li>
<li>
<p>Spend a <em>lot</em> of time on essays. Think of your essay writing time as paying $10K per hour, because it probably has that much of an effect on your potential future earnings.</p>
</li>
<li>Brag like crazy on your essay. Our family culture encourages being humble, and this did <em>not</em> serve us well in writing essays. Your essay is competing against people who are straight-up exaggerating their accomplishments. Do not give them any advantage by being modest.</li>
</ul>
<h2 id="things-we-didnt-do-because-they-didnt-matter">Things we didn’t do, because they didn’t matter</h2>
<ul>
<li>Visit campuses. Our goal was to go to the best school that would accept us, so we didn’t spend any time considering which school had the nicest amenities or campus until after we had acceptance letters.</li>
</ul>
<h1 id="deciding-among-several-admission-offers">Deciding among several admission offers</h1>
<p>Each of my kids was accepted at several schools. In deciding which school to attend, they considered the following, from most important to least important:</p>
<ol>
<li>Whether they were directly admitted to the CS department, and if not, what were the realistic chances of being admitted in the future.</li>
<li>The school’s CS department ranking.</li>
<li>The school’s over-all academic ranking.</li>
<li>Location relative to US tech hubs.</li>
<li>Cost, including travel and housing.</li>
<li>Quality of life.</li>
</ol>
<p>One child chose to attend the second-best school that accepted them because the higher-ranked school:</p>
<ul>
<li>did not curve grades.
<ul>
<li>Attending that school would have likely resulted in a lower GPA which could have made transferring to a different school, getting an internship, job, or attending graduate school more difficult.</li>
</ul>
</li>
<li>was further from home.</li>
<li>was further from tech hubs.</li>
<li>was in a rural area.</li>
</ul>
<h2 id="cs-vs-cs-adjacent-degrees">CS vs. CS-adjacent degrees</h2>
<p>Two of my kids had to decide between a CS degree at a less desirable school vs. a non-CS degree at a more desirable school.</p>
<p>Many schools with over-subscribed CS departments offer a variety of CS-adjacent degrees, such as Math, Electrical Engineering, Human Computer Interaction, and Data Science. Some of these CS-adjacent degrees allow you to take beginning CS department courses, providing you with a basic CS education.</p>
<p>We spent a lot of time considering the pros and cons.</p>
<p>Arguments in favor of choosing to pursue a CS degree at a less desirable school:</p>
<ul>
<li>Get to study CS full time.</li>
<li>More likely to hear about CS internship and job opportunities.</li>
<li>Easier to get into CS classes.</li>
<li>Simpler story when applying for CS jobs and internships.</li>
</ul>
<p>Arguments in favor of choosing a CS-adjacent degree at a more desirable school:</p>
<ul>
<li>Many employers care more about the school name than the degree name.</li>
<li>You can potentially take CS courses as electives.</li>
<li>Don’t have to take the higher level academic-oriented CS classes.</li>
<li>You might get a better education over-all.</li>
<li>If you decide you don’t like CS, you’re in a better position.</li>
</ul>
<p>In the end, my kids who were presented with this choice chose the CS degree program at the less desirable school, because it seemed like the safer, less-stressful course.</p>
<h1 id="final-words-of-advice">Final words of advice</h1>
<p>CS admissions is becoming increasingly competitive. While this will eventually change, it’s likely that the 2023 and 2024 admission cycles will be even more difficult than 2022. Plan ahead and put in as much time and effort as you can. Good luck to you!</p>Jack Palevichtl/dr advice to kids and parents aiming for admission to a good undergraduate CS program in 2022-2023: Research what your target schools are looking for. Calculate the cost/benefit ratio. Have a back-up plan. Having just helped my three kids get into undergraduate CS programs at, respectively a top-10, a top-25, and a top-35 school, I want to share my family’s experience.A Solver for Hitman Go Levels2022-04-29T00:00:00-07:002022-04-29T00:00:00-07:00https://jackpal.github.io/2022/04/29/A_Solver_for_Hitman_Go_Levels<p>I wrote a solver for <a href="https://en.wikipedia.org/wiki/Hitman_Go">Hitman Go</a>
levels. You can use it to:</p>
<ul>
<li>Solve existing levels.</li>
<li>Design and test your own levels.</li>
<li>Study graph search algorithms like A-Star.</li>
</ul>
<p>The code is available in four related projects:</p>
<ul>
<li><a href="https://github.com/jackpal/SpyPuzzleGameState">SpyPuzzleGameState</a>
The data structures for game levels.</li>
<li><a href="https://github.com/jackpal/SpyPuzzleSolver">SpyPuzzleSolver</a> An A-Star-based level solver.</li>
<li><a href="https://github.com/jackpal/SpyPuzzleCLI">SpyPuzzleCLI</a> A command-line tool for solving levels.</li>
<li><a href="https://github.com/jackpal/SpyPuzzleApp">SpyPuzzleApp</a> An iOS app for interactively testing and solving levels.</li>
</ul>
<p><img src="/assets/posts/2022-04-29-SpyPuzzleApp.png" alt="Screenshot of Spy Puzzle App" /></p>
<p>In this screenshot of a simple test level, the “Agent” needs to pick up
a red key to open the red door, followed by the blue key to open the blue
door. The solver has correctly solved the moves required to solve the
level as “east, east, south”.</p>
<!--more-->
<p>This project started over the 2021 holiday break. I was playing
<a href="https://en.wikipedia.org/wiki/Hitman_Go">Hitman Go</a>, and I was stuck on
a difficult level. I decided to write a little Python script to solve the
level. It only took a few hours to write the script.</p>
<p>That naturally lead to extending the script to solve more levels, then
switching to Swift for type safety, then writing a UI to make it
easier to debug, and here we are today.</p>
<p>The code can represent any Hitman Go level, and knows the rules for the
game. All node types, edge types, enemy types, and items are represented.</p>
<p>In theory the solver should be able to
solve any solvable game level. It can solve some levels very quickly,
while other levels are effectively unsolvable due to taking too much time or
memory. I am using the A-Star algorithm and some simple heuristics.</p>
<p>The current v 0.1.0 solver can quickly solve over half of all Hitman Go levels:</p>
<table>
<thead>
<tr>
<th>Status</th>
<th style="text-align: right">Count</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fast</td>
<td style="text-align: right">66</td>
</tr>
<tr>
<td>Too slow</td>
<td style="text-align: right">25</td>
</tr>
<tr>
<td>Not tested</td>
<td style="text-align: right">21</td>
</tr>
<tr>
<td>Total</td>
<td style="text-align: right">121</td>
</tr>
</tbody>
</table>
<p>The “not tested” levels are levels that seem more complicated than the
“too slow” levels, so I haven’t added them to the test suite yet. I will
add them once the “too slow” levels have been solved.</p>
<p>The typical reason that a level can’t be solved in reasonable time is that
there is an important item (like a costume) locked behind a door. The
level solver doesn’t understand that, so it take a long time exploring
dead ends before it tries to open the door.</p>
<h1 id="benefits-and-drawbacks-of-using-swift">Benefits and drawbacks of using Swift</h1>
<p>I started the project in Python, but switched to Swift because the Python
code quickly became difficult to work with.</p>
<p>Swift was a good choice:</p>
<ul>
<li>strong type checking</li>
<li>enum types</li>
<li>value semantics for structs, Arrays, Sets, and Dictionaries</li>
<li>code-generating conformances for Hashable</li>
<li>A nice third party <a href="https://github.com/Dev1an/A-Star">A-Star</a> implementation.</li>
<li>Swift UI made it easy to write an app for testing.</li>
<li>Swift Package Manager and Xcode made development and regression testing easy.</li>
</ul>
<p>However there were some drawbacks to using Swift:</p>
<ul>
<li>Debug builds were slow. I developed using Release mode except when debugging.</li>
<li>The value semantics seemed to be slow and use a lot of memory.</li>
</ul>
<p>I think that a C++ implementation would probably
use less memory and run faster.</p>
<h1 id="future-work">Future work</h1>
<p>If I had time, I would like to improve the solver to try solving more
levels. Some sort of “sub goal” strategy seems like it would be useful.</p>Jack PalevichI wrote a solver for Hitman Go levels. You can use it to: Solve existing levels. Design and test your own levels. Study graph search algorithms like A-Star. The code is available in four related projects: SpyPuzzleGameState The data structures for game levels. SpyPuzzleSolver An A-Star-based level solver. SpyPuzzleCLI A command-line tool for solving levels. SpyPuzzleApp An iOS app for interactively testing and solving levels. In this screenshot of a simple test level, the “Agent” needs to pick up a red key to open the red door, followed by the blue key to open the blue door. The solver has correctly solved the moves required to solve the level as “east, east, south”.Calming Ripples App2022-03-20T00:00:00-07:002022-03-20T00:00:00-07:00https://jackpal.github.io/2022/03/20/Calming-Ripples<p>Calming Ripples is a SwiftUI app that lets you draw animated ripples.</p>
<p><a href="https://apps.apple.com/us/app/calming-ripples/id1615302570">Available for iOS, iPadOS and macOS.</a></p>
<p><a href="https://github.com/jackpal/Ripples">Study the source code</a> to learn these techniques:</p>
<ul>
<li>Handle multi-finger touch events.</li>
<li>Draw complex 2D designs using the SwiftUI Canvas view.</li>
<li>Animate using the SwiftUI TimelineView view.</li>
<li>Use the onChanged() view method to create a dynamically changing animation.</li>
</ul>
<p>You could use the techniques in this project to create a 2D game.</p>
<p><img src="/assets/posts/2022-03-20-Ripples.png" alt="Screenshot of Calming Ripples App" /></p>
<!--more-->
<h1 id="general-app-architecture">General app architecture</h1>
<p>The app architecture is:</p>
<ul>
<li>Model structs and classes:
<ul>
<li>contain the data that’s going to be animated.</li>
</ul>
</li>
<li>View structs:
<ul>
<li>display the model data.</li>
<li>animate the model data.</li>
<li>interpret user gestures to modify the model data.</li>
</ul>
</li>
</ul>
<h2 id="model-data">Model Data</h2>
<p>For this application, the model is just an array of ripples.</p>
<h3 id="struct-ripple">struct Ripple</h3>
<p>A ripple contains a center, a start time, and a color. This representation was chosen to be easy to animate.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>
<span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">typealias</span> <span class="kt">RippleColor</span> <span class="o">=</span> <span class="kt">SIMD3</span><span class="o"><</span><span class="kt">Float</span><span class="o">></span>
<span class="kd">struct</span> <span class="kt">Ripple</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">center</span><span class="p">:</span> <span class="kt">CGPoint</span>
<span class="k">var</span> <span class="nv">start</span><span class="p">:</span> <span class="kt">Date</span>
<span class="k">var</span> <span class="nv">color</span><span class="p">:</span> <span class="kt">RippleColor</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">SIMD3</span> <span class="k">where</span> <span class="kt">Scalar</span> <span class="o">==</span> <span class="kt">Float</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">color</span> <span class="p">:</span> <span class="kt">Color</span> <span class="p">{</span>
<span class="kt">Color</span><span class="p">(</span><span class="nv">red</span><span class="p">:</span><span class="kt">Double</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nv">green</span><span class="p">:</span> <span class="kt">Double</span><span class="p">(</span><span class="n">y</span><span class="p">),</span> <span class="nv">blue</span><span class="p">:</span> <span class="kt">Double</span><span class="p">(</span><span class="n">z</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="class-model">class Model</h3>
<p>A model contains an array of ripples, stored in creation-time order. The total number of ripples is capped,
to avoid animation glitches if too many ripples are drawn at the same time.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Combine</span>
<span class="kd">import</span> <span class="kt">Foundation</span>
<span class="kd">class</span> <span class="kt">Model</span> <span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
<span class="kd">@Published</span> <span class="k">var</span> <span class="nv">ripples</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Ripple</span><span class="p">]()</span>
<span class="c1">// Maximum number of ripples.</span>
<span class="k">var</span> <span class="nv">maximumNumberOfRipples</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">maximumNumberOfRipples</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maximumNumberOfRipples</span> <span class="o">=</span> <span class="n">maximumNumberOfRipples</span>
<span class="p">}</span>
<span class="k">var</span> <span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span> <span class="p">{</span> <span class="n">ripples</span><span class="o">.</span><span class="n">count</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">isEmpty</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span> <span class="n">ripples</span><span class="o">.</span><span class="n">isEmpty</span> <span class="p">}</span>
<span class="kd">func</span> <span class="nf">append</span><span class="p">(</span><span class="nv">ripple</span><span class="p">:</span> <span class="kt">Ripple</span><span class="p">)</span> <span class="p">{</span>
<span class="n">ripples</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">ripple</span><span class="p">)</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">maximumNumberOfRipples</span> <span class="o">=</span> <span class="n">maximumNumberOfRipples</span><span class="p">,</span> <span class="n">ripples</span><span class="o">.</span><span class="n">count</span> <span class="o">></span> <span class="n">maximumNumberOfRipples</span> <span class="p">{</span>
<span class="n">ripples</span><span class="o">.</span><span class="nf">removeFirst</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">trim</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="kt">Date</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">first</span> <span class="o">=</span> <span class="n">ripples</span><span class="o">.</span><span class="n">first</span><span class="p">,</span> <span class="n">first</span><span class="o">.</span><span class="n">start</span> <span class="o"><</span> <span class="n">start</span> <span class="p">{</span>
<span class="n">ripples</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">ripples</span><span class="o">.</span><span class="n">drop</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">start</span> <span class="o"><</span> <span class="n">start</span> <span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h1 id="views">Views</h1>
<p>The app uses several views:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>ContentView</td>
<td>The main app view.</td>
</tr>
<tr>
<td>Metrics</td>
<td>Displays the number of active ripples. Only used in debug builds.</td>
</tr>
<tr>
<td>NFingerTapView</td>
<td>Utility view for handling multi-finger input. Reusable in other apps.</td>
</tr>
<tr>
<td>Pond</td>
<td>Draws the ripples from the Model.</td>
</tr>
</tbody>
</table>
<h2 id="contentview">ContentView</h2>
<p>The content view has a few responsibilities:</p>
<ul>
<li>It holds the model.</li>
<li>It contains the TimelineView that does the animation.
<ul>
<li>Note that the animation is paused when the model is empty.</li>
</ul>
</li>
<li>Uses the Pond view to draw the model.</li>
<li>For debug builds, composites the Metrics over the Pond.</li>
</ul>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@StateObject</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">model</span> <span class="o">=</span> <span class="kt">Model</span><span class="p">(</span><span class="nv">maximumNumberOfRipples</span><span class="p">:</span> <span class="mi">1000</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="nf">animation</span><span class="p">(</span><span class="nv">minimumInterval</span><span class="p">:</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="mi">120</span><span class="p">,</span> <span class="nv">paused</span><span class="p">:</span> <span class="n">model</span><span class="o">.</span><span class="n">isEmpty</span><span class="p">))</span> <span class="p">{</span> <span class="n">timeline</span> <span class="k">in</span>
<span class="kt">ZStack</span> <span class="p">{</span>
<span class="kt">Pond</span><span class="p">(</span><span class="nv">model</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="nv">date</span><span class="p">:</span><span class="n">timeline</span><span class="o">.</span><span class="n">date</span><span class="p">)</span>
<span class="o">.</span><span class="nf">ignoresSafeArea</span><span class="p">()</span>
<span class="cp">#if DEBUG</span>
<span class="kt">Metrics</span><span class="p">(</span><span class="nv">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span>
<span class="cp">#endif</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="o">.</span><span class="n">white</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="metrics">Metrics</h2>
<p>This view displays the number of active ripples. It’s useful for debugging the code for adding and removing ripples. It’s only shown
for debug builds.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">struct</span> <span class="kt">Metrics</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@ObservedObject</span>
<span class="k">var</span> <span class="nv">model</span><span class="p">:</span><span class="kt">Model</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">metricsOpacity</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Ripples: </span><span class="se">\(</span><span class="n">model</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="o">.</span><span class="n">largeTitle</span><span class="p">,</span> <span class="nv">design</span><span class="p">:</span> <span class="o">.</span><span class="n">rounded</span><span class="p">)</span><span class="o">.</span><span class="nf">monospacedDigit</span><span class="p">())</span>
<span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="o">.</span><span class="n">accentColor</span><span class="p">)</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">leading</span><span class="p">)</span>
<span class="kt">Spacer</span><span class="p">()</span>
<span class="p">}</span>
<span class="kt">Spacer</span><span class="p">()</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="n">metricsOpacity</span><span class="p">)</span>
<span class="o">.</span><span class="nf">onChange</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">model</span><span class="o">.</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span> <span class="n">isEmpty</span> <span class="k">in</span>
<span class="n">withAnimation</span> <span class="p">{</span>
<span class="n">metricsOpacity</span> <span class="o">=</span> <span class="n">isEmpty</span> <span class="p">?</span> <span class="mf">0.0</span> <span class="p">:</span> <span class="mf">1.0</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="nfingertapview">NFingerTapView</h2>
<p>This is a utility view that keeps track of ongoing touch gestures. It can be reused in other applications.</p>
<p>See the Pond view, below, for an example of how to use NFingerTapView.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>
<span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">import</span> <span class="kt">UIKit</span>
<span class="c1">/// Adapted from https://stackoverflow.com/questions/61566929/swiftui-multitouch-gesture-multiple-gestures</span>
<span class="kd">class</span> <span class="kt">NFingerGestureRecognizer</span><span class="p">:</span> <span class="kt">UIGestureRecognizer</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">tappedCallback</span><span class="p">:</span> <span class="p">(</span><span class="kt">UITouch</span><span class="p">,</span> <span class="kt">CGPoint</span><span class="p">?)</span> <span class="o">-></span> <span class="kt">Void</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">target</span><span class="p">:</span> <span class="kt">Any</span><span class="p">?,</span> <span class="nv">tappedCallback</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">UITouch</span><span class="p">,</span> <span class="kt">CGPoint</span><span class="p">?)</span> <span class="o">-></span> <span class="p">())</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">tappedCallback</span> <span class="o">=</span> <span class="n">tappedCallback</span>
<span class="k">super</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">target</span><span class="p">:</span> <span class="n">target</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">touchesBegan</span><span class="p">(</span><span class="n">_</span> <span class="nv">touches</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UITouch</span><span class="o">></span><span class="p">,</span> <span class="n">with</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">touch</span> <span class="k">in</span> <span class="n">touches</span> <span class="p">{</span>
<span class="nf">tappedCallback</span><span class="p">(</span><span class="n">touch</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="nf">location</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">touch</span><span class="o">.</span><span class="n">view</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">touchesMoved</span><span class="p">(</span><span class="n">_</span> <span class="nv">touches</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UITouch</span><span class="o">></span><span class="p">,</span> <span class="n">with</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">touch</span> <span class="k">in</span> <span class="n">touches</span> <span class="p">{</span>
<span class="nf">tappedCallback</span><span class="p">(</span><span class="n">touch</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="nf">location</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">touch</span><span class="o">.</span><span class="n">view</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">touchesEnded</span><span class="p">(</span><span class="n">_</span> <span class="nv">touches</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UITouch</span><span class="o">></span><span class="p">,</span> <span class="n">with</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">touch</span> <span class="k">in</span> <span class="n">touches</span> <span class="p">{</span>
<span class="nf">tappedCallback</span><span class="p">(</span><span class="n">touch</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">touchesCancelled</span><span class="p">(</span><span class="n">_</span> <span class="nv">touches</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UITouch</span><span class="o">></span><span class="p">,</span> <span class="n">with</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">touch</span> <span class="k">in</span> <span class="n">touches</span> <span class="p">{</span>
<span class="nf">tappedCallback</span><span class="p">(</span><span class="n">touch</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">NFingerTapView</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">tappedCallback</span><span class="p">:</span> <span class="p">(</span><span class="kt">UITouch</span><span class="p">,</span> <span class="kt">CGPoint</span><span class="p">?)</span> <span class="o">-></span> <span class="kt">Void</span>
<span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">UIViewRepresentableContext</span><span class="o"><</span><span class="kt">NFingerTapView</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="kt">NFingerTapView</span><span class="o">.</span><span class="kt">UIViewType</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">v</span> <span class="o">=</span> <span class="kt">UIView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">gesture</span> <span class="o">=</span> <span class="kt">NFingerGestureRecognizer</span><span class="p">(</span><span class="nv">target</span><span class="p">:</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="p">,</span> <span class="nv">tappedCallback</span><span class="p">:</span> <span class="n">tappedCallback</span><span class="p">)</span>
<span class="n">v</span><span class="o">.</span><span class="nf">addGestureRecognizer</span><span class="p">(</span><span class="n">gesture</span><span class="p">)</span>
<span class="k">return</span> <span class="n">v</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UIView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">UIViewRepresentableContext</span><span class="o"><</span><span class="kt">NFingerTapView</span><span class="o">></span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// empty</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="pond">Pond</h2>
<p>The pond view has three responsibilities:</p>
<ul>
<li>Display the model using a Canvas view.</li>
<li>Change the model state when the current time changes by using an onChanged() method.</li>
<li>Add ripples to the model when the user touches the display by using an NFingerTapView.</li>
</ul>
<p>The key trick for creating a SwiftUI-based simulation or animation or game is to use <code class="language-plaintext highlighter-rouge">.onChange(of: date)</code> to update the model.
(The date is updated by the enclosing TimelineView.)</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">rWhite</span> <span class="o">=</span> <span class="kt">RippleColor</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span><span class="mf">1.0</span><span class="p">,</span><span class="mf">1.0</span><span class="p">)</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">kColors</span> <span class="o">=</span> <span class="p">[</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">0.4627</span><span class="p">,</span> <span class="mf">0.8392</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">0.8392</span><span class="p">,</span> <span class="mf">0.4627</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">0.4627</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.8392</span><span class="p">),</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.4627</span><span class="p">,</span> <span class="mf">0.8392</span><span class="p">),</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.8392</span><span class="p">,</span> <span class="mf">0.4627</span><span class="p">),</span>
<span class="kt">RippleColor</span><span class="p">(</span><span class="mf">0.8392</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.4627</span><span class="p">)</span>
<span class="p">]</span>
<span class="kd">struct</span> <span class="kt">Pond</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">lifetime</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mi">14</span>
<span class="kd">@ObservedObject</span>
<span class="k">var</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">Model</span>
<span class="kd">@State</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">touches</span> <span class="o">=</span> <span class="p">[</span><span class="kt">UITouch</span><span class="p">:</span><span class="kt">RippleColor</span><span class="p">]()</span>
<span class="k">let</span> <span class="nv">date</span><span class="p">:</span> <span class="kt">Date</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">ZStack</span> <span class="p">{</span>
<span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
<span class="k">for</span> <span class="n">ripple</span> <span class="k">in</span> <span class="n">model</span><span class="o">.</span><span class="n">ripples</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="p">(</span><span class="n">date</span><span class="o">.</span><span class="nf">timeIntervalSince</span><span class="p">(</span><span class="n">ripple</span><span class="o">.</span><span class="n">start</span><span class="p">))</span> <span class="o">*</span> <span class="mf">40.0</span>
<span class="k">let</span> <span class="nv">diameter</span> <span class="o">=</span> <span class="n">radius</span> <span class="o">*</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">x0</span> <span class="o">=</span> <span class="n">ripple</span><span class="o">.</span><span class="n">center</span><span class="o">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">radius</span>
<span class="k">let</span> <span class="nv">y0</span> <span class="o">=</span> <span class="n">ripple</span><span class="o">.</span><span class="n">center</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">radius</span>
<span class="k">let</span> <span class="nv">rect</span> <span class="o">=</span> <span class="kt">CGRect</span><span class="p">(</span><span class="nv">origin</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">x0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">y0</span><span class="p">),</span> <span class="nv">size</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">diameter</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">diameter</span><span class="p">))</span>
<span class="k">let</span> <span class="nv">circle</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">(</span><span class="nv">ellipseIn</span><span class="p">:</span> <span class="n">rect</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">fade</span><span class="p">:</span> <span class="kt">Float</span> <span class="o">=</span> <span class="kt">Float</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">min</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">date</span><span class="o">.</span><span class="nf">timeIntervalSince</span><span class="p">(</span><span class="n">ripple</span><span class="o">.</span><span class="n">start</span><span class="p">)</span> <span class="o">/</span> <span class="n">lifetime</span><span class="p">)))</span>
<span class="k">let</span> <span class="nv">color</span> <span class="o">=</span> <span class="p">((</span><span class="mf">1.0</span> <span class="o">-</span> <span class="n">fade</span><span class="p">)</span> <span class="o">*</span> <span class="n">ripple</span><span class="o">.</span><span class="n">color</span> <span class="o">+</span> <span class="n">rWhite</span> <span class="o">*</span> <span class="n">fade</span><span class="p">)</span><span class="o">.</span><span class="n">color</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="n">circle</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="nf">color</span><span class="p">(</span><span class="n">color</span><span class="p">),</span> <span class="nv">lineWidth</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">NFingerTapView</span> <span class="p">{</span> <span class="n">touch</span><span class="p">,</span> <span class="n">location</span> <span class="k">in</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">location</span> <span class="o">=</span> <span class="n">location</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">color</span><span class="p">:</span> <span class="kt">RippleColor</span><span class="p">?</span> <span class="o">=</span> <span class="n">touches</span><span class="p">[</span><span class="n">touch</span><span class="p">]</span>
<span class="k">if</span> <span class="n">color</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="n">color</span> <span class="o">=</span> <span class="n">kColors</span><span class="p">[</span><span class="n">touches</span><span class="o">.</span><span class="n">count</span> <span class="o">%</span> <span class="n">kColors</span><span class="o">.</span><span class="n">count</span><span class="p">]</span>
<span class="n">touches</span><span class="p">[</span><span class="n">touch</span><span class="p">]</span> <span class="o">=</span> <span class="n">color</span>
<span class="p">}</span>
<span class="n">model</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="nv">ripple</span><span class="p">:</span><span class="kt">Ripple</span><span class="p">(</span><span class="nv">center</span><span class="p">:</span><span class="n">location</span><span class="p">,</span> <span class="nv">start</span><span class="p">:</span><span class="kt">Date</span><span class="p">(),</span> <span class="nv">color</span><span class="p">:</span><span class="n">color</span><span class="o">!</span><span class="p">))</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">touches</span><span class="o">.</span><span class="nf">removeValue</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span><span class="n">touch</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">onChange</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">date</span><span class="p">)</span> <span class="p">{</span> <span class="n">date</span> <span class="k">in</span>
<span class="k">let</span> <span class="nv">deadline</span> <span class="o">=</span> <span class="n">date</span> <span class="o">-</span> <span class="n">lifetime</span>
<span class="n">model</span><span class="o">.</span><span class="nf">trim</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span><span class="n">deadline</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h1 id="things-you-can-do">Things you can do</h1>
<p>The sky’s the limit, but here’s some things you can do to modify this app:</p>
<ul>
<li>Change the maximum number of ripples. It’s currently capped at 1000 in order to display smoothly on slower devices.</li>
<li>Change the way ripples are drawn. Why not square ripples?</li>
<li>Cycle the colors of the ripples.</li>
<li>Simulate water current, wind or gravity by changing the position of the ripple centers over time.</li>
<li>Add new kinds of objects, like leaves, or fish.</li>
</ul>
<p>You can also use the basic structure of a model, a Canvas, a TimelineView and a NFingerTapView to build many kinds of 2D animations or games.</p>
<p>For general animation, it’s helpful to calculate the “delta” time interval since the last animation. You can do that by:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@State</span> <span class="k">var</span> <span class="nv">lastDate</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="o">.</span><span class="nf">onChange</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">date</span><span class="p">)</span> <span class="p">{</span> <span class="n">date</span> <span class="k">in</span>
<span class="k">let</span> <span class="nv">delta</span> <span class="o">=</span> <span class="n">date</span><span class="o">.</span><span class="nf">timeIntervalSince</span><span class="p">(</span><span class="n">lastDate</span><span class="p">)</span>
<span class="n">lastDate</span> <span class="o">=</span> <span class="n">date</span>
<span class="c1">// use delta to update the model...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Jack PalevichCalming Ripples is a SwiftUI app that lets you draw animated ripples. Available for iOS, iPadOS and macOS. Study the source code to learn these techniques: Handle multi-finger touch events. Draw complex 2D designs using the SwiftUI Canvas view. Animate using the SwiftUI TimelineView view. Use the onChanged() view method to create a dynamically changing animation. You could use the techniques in this project to create a 2D game.Animating along a SwiftUI Path2022-02-13T00:00:00-08:002022-02-13T00:00:00-08:00https://jackpal.github.io/2022/02/13/Animating_along_a_SwiftUI_Path<p>We can use trigonometry and finite differences to animate rigid objects along a SwiftUI path.</p>
<p>The SwiftUI Path class is missing several useful methods for evaluating properties of a path:</p>
<ul>
<li>finding the position (as a CGPoint) of a given fractional position of the path.</li>
<li>finding the heading (as an angle) of a given fractional position of the path.</li>
<li>finding the total length of the path, measured in points.</li>
</ul>
<p>Happily, we can write these methods based on the existing <code class="language-plaintext highlighter-rouge">trimmedPath</code> method.</p>
<p>With the aid of these methods it’s possible to
create animations that move rigid bodies along arbitrary paths.</p>
<!--more-->
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">defaultEpsilon</span> <span class="o">=</span> <span class="mf">1e-7</span>
<span class="kd">extension</span> <span class="kt">Path</span> <span class="p">{</span>
<span class="c1">/// Returns the position for a point on the path with the given</span>
<span class="c1">/// fractional path value between 0 and 1.</span>
<span class="kd">func</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span>
<span class="nv">epsilon</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="n">defaultEpsilon</span><span class="p">,</span>
<span class="nv">closed</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="c1">// Make sure a and b don't go outside the bounds 0 ... 1.0</span>
<span class="k">var</span> <span class="nv">a</span> <span class="o">=</span> <span class="n">at</span>
<span class="k">var</span> <span class="nv">b</span> <span class="o">=</span> <span class="n">at</span> <span class="o">+</span> <span class="n">epsilon</span>
<span class="k">if</span> <span class="n">closed</span> <span class="p">{</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">b</span> <span class="o">></span> <span class="mf">1.0</span> <span class="p">{</span>
<span class="n">b</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="n">a</span> <span class="o">=</span> <span class="n">b</span> <span class="o">-</span> <span class="n">epsilon</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">littlePieceOfPathFromAToB</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">trimmedPath</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">a</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">b</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">boundsOfLittlePiece</span> <span class="o">=</span> <span class="n">littlePieceOfPathFromAToB</span><span class="o">.</span><span class="n">boundingRect</span>
<span class="k">let</span> <span class="nv">positionOfA</span> <span class="o">=</span> <span class="n">boundsOfLittlePiece</span><span class="o">.</span><span class="n">origin</span>
<span class="k">return</span> <span class="n">positionOfA</span>
<span class="p">}</span>
<span class="c1">/// Returns the tangent angle in radians for a given fractional path value between 0 and 1.</span>
<span class="c1">/// The tangent angle ranges from -π to π.</span>
<span class="c1">/// An angle of 0 means the curve is pointing in the positive X direction.</span>
<span class="c1">/// The angle increases in the clockwise direction.</span>
<span class="kd">func</span> <span class="nf">evaluateTangent</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span>
<span class="nv">lookAhead</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="n">defaultEpsilon</span><span class="p">,</span>
<span class="nv">closed</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">a</span> <span class="o">=</span> <span class="n">at</span>
<span class="k">var</span> <span class="nv">b</span> <span class="o">=</span> <span class="n">at</span> <span class="o">+</span> <span class="n">lookAhead</span>
<span class="k">if</span> <span class="n">closed</span> <span class="p">{</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">b</span> <span class="o">></span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">lookAhead</span> <span class="p">{</span>
<span class="n">b</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">lookAhead</span>
<span class="n">a</span> <span class="o">=</span> <span class="n">b</span> <span class="o">-</span> <span class="n">lookAhead</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">pa</span> <span class="o">=</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">a</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">pb</span> <span class="o">=</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">b</span><span class="p">)</span>
<span class="k">return</span> <span class="nf">atan2</span><span class="p">(</span><span class="n">pb</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">pa</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="n">pb</span><span class="o">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">pa</span><span class="o">.</span><span class="n">x</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">/// Return the path length in pixels.</span>
<span class="k">var</span> <span class="nv">pathLength</span> <span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">epsilon</span> <span class="o">=</span> <span class="mf">1e-7</span>
<span class="k">let</span> <span class="nv">sampleParameter</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">let</span> <span class="nv">a</span> <span class="o">=</span> <span class="n">sampleParameter</span>
<span class="k">let</span> <span class="nv">b</span> <span class="o">=</span> <span class="n">sampleParameter</span> <span class="o">+</span> <span class="n">epsilon</span>
<span class="k">let</span> <span class="nv">littlePieceOfPathFromAToB</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">trimmedPath</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">a</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">b</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">boundsOfLittlePiece</span> <span class="o">=</span> <span class="n">littlePieceOfPathFromAToB</span><span class="o">.</span><span class="n">boundingRect</span>
<span class="k">let</span> <span class="nv">dx</span> <span class="o">=</span> <span class="n">boundsOfLittlePiece</span><span class="o">.</span><span class="n">width</span>
<span class="k">let</span> <span class="nv">dy</span> <span class="o">=</span> <span class="n">boundsOfLittlePiece</span><span class="o">.</span><span class="n">height</span>
<span class="k">let</span> <span class="nv">distance</span> <span class="o">=</span> <span class="nf">sqrt</span><span class="p">(</span><span class="n">dx</span> <span class="o">*</span> <span class="n">dx</span> <span class="o">+</span> <span class="n">dy</span> <span class="o">*</span> <span class="n">dy</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">pathLengthEstimate</span> <span class="o">=</span> <span class="n">distance</span> <span class="o">/</span> <span class="n">epsilon</span>
<span class="k">return</span> <span class="n">pathLengthEstimate</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here’s an example SwiftUI view that animates a short word along an arbitrary path:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">createPath</span><span class="p">()</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">p</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="n">p</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">20</span><span class="p">))</span>
<span class="n">p</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">20</span><span class="p">))</span>
<span class="n">p</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">100</span><span class="p">))</span>
<span class="n">p</span><span class="o">.</span><span class="nf">addCurve</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span><span class="mi">100</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">400</span><span class="p">),</span>
<span class="nv">control1</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">200</span><span class="p">),</span>
<span class="nv">control2</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span><span class="mi">200</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">300</span><span class="p">))</span>
<span class="n">p</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">300</span><span class="p">))</span>
<span class="k">return</span> <span class="n">p</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">startDate</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">path</span> <span class="o">=</span> <span class="nf">createPath</span><span class="p">()</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">animationDuration</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mi">10</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">epsilon</span> <span class="o">=</span> <span class="mf">0.0001</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="nf">animation</span><span class="p">(</span><span class="nv">minimumInterval</span><span class="p">:</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="mi">120</span><span class="p">))</span> <span class="p">{</span> <span class="n">timeline</span> <span class="k">in</span>
<span class="k">let</span> <span class="nv">elapsed</span> <span class="o">=</span> <span class="n">timeline</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="nf">timeIntervalSince</span><span class="p">(</span><span class="n">startDate</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">animationProgress</span> <span class="o">=</span>
<span class="n">elapsed</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="n">animationDuration</span><span class="p">)</span>
<span class="o">/</span> <span class="n">animationDuration</span>
<span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
<span class="n">context</span><span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span><span class="o">.</span><span class="nf">color</span><span class="p">(</span><span class="o">.</span><span class="n">green</span><span class="p">))</span>
<span class="k">let</span> <span class="nv">pos</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span><span class="n">animationProgress</span><span class="p">)</span>
<span class="c1">// Use a lookAhead to have the car smoothly animate around sharp corners</span>
<span class="k">let</span> <span class="nv">tangentAngle</span> <span class="o">=</span>
<span class="n">path</span><span class="o">.</span><span class="nf">evaluateTangent</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">animationProgress</span><span class="p">,</span> <span class="nv">lookAhead</span><span class="p">:</span><span class="mf">0.01</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">oldTransform</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">transform</span>
<span class="n">context</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="n">oldTransform</span>
<span class="o">.</span><span class="nf">translatedBy</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">pos</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">pos</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
<span class="o">.</span><span class="nf">rotated</span><span class="p">(</span><span class="nv">by</span><span class="p">:</span><span class="n">tangentAngle</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="kt">Text</span><span class="p">(</span><span class="s">"car"</span><span class="p">),</span> <span class="nv">at</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span><span class="o">-</span><span class="mi">8</span><span class="p">))</span>
<span class="n">context</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="n">oldTransform</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Jack PalevichWe can use trigonometry and finite differences to animate rigid objects along a SwiftUI path. The SwiftUI Path class is missing several useful methods for evaluating properties of a path: finding the position (as a CGPoint) of a given fractional position of the path. finding the heading (as an angle) of a given fractional position of the path. finding the total length of the path, measured in points. Happily, we can write these methods based on the existing trimmedPath method. With the aid of these methods it’s possible to create animations that move rigid bodies along arbitrary paths.Tailscale and Tablo2022-02-13T00:00:00-08:002022-02-13T00:00:00-08:00https://jackpal.github.io/2022/02/13/Tailscale_and_Tablo<p>My son’s away at college, without a TV. He wanted to watch the Superbowl. We
realized that one way to do that would be for him to access our family’s
<a href="https://www.tablotv.com/">Tablo DVR</a> remotely. Tablo supports remote access,
but there’s a catch: The client software has to be set up while the Tablo device
and the client machine are on the same local network.</p>
<p>But my son was 1400 miles away.</p>
<p>This seemed like a good opportunity to experiment with a <a href="https://tailscale.com/">Tailscale</a>
private network. And therein lies a tale.</p>
<!--more-->
<p>If, like me, you’ve been <a href="https://twitter.com/tailscale">following the Tailscale company on Twitter</a>,
you probably expected me to report that it took only a few minutes to set up Tailscale,
and that everything went super smoothly. Unfortunately, while everything worked out
fine in the end, it took longer than the typical Tailscale success story.</p>
<p>The initial Tailscale enrollment went super smoothly, as everyone reports. But I soon
identified an issue: The Tablo DVR does not allow for third party software installation.
Therefore, in order to give my son access to it, I would need to configure a Tailscale
<a href="https://tailscale.com/kb/1019/subnets">subnet router</a>. And currently that feature
only works on a Linux host.</p>
<p>I don’t currently run any Linux devices that are capable of installing Tailscale.
But I do have a Windows PC.</p>
<p>I decided to temporarily (just for the Superbowl) repurpose the Windows PC into a Linux box.</p>
<p>These day’s I’m more comfortable with Mac / Linux than with Windows, so the following
uses the Mac for several steps where other people would probably use Windows.</p>
<p>To create a temporary Linux Tailscale subnet router, I did the following:</p>
<ol>
<li>Downloaded a <a href="https://debian.org/CD/live/">Debian Live CD image</a> to my Mac.</li>
<li>Used the <a href="https://www.balena.io/etcher/">Balena Etcher</a> app to create a bootable USB drive.</li>
<li>Reconfigured the PC’s BIOS to boot from the USB drive.</li>
<li>Went through the Debian Live CD configuration process.</li>
<li>Speed-ran the Tailscale installation for a Linux box:</li>
</ol>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># The first two lines are specific to Debian bullseye, see Tailscale docs for other releases:</span>
curl <span class="nt">-fsSL</span> https://pkgs.tailscale.com/stable/debian/bullseye.noarmor.gpg | <span class="nb">sudo tee</span> /usr/share/keyrings/tailscale-archive-keyring.gpg <span class="o">></span>/dev/null
curl <span class="nt">-fsSL</span> https://pkgs.tailscale.com/stable/debian/bullseye.tailscale-keyring.list | <span class="nb">sudo tee
sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install </span>tailscale
<span class="c"># I found I had to do the following, even though it's not documented.</span>
<span class="nb">sudo </span>systemctl start tailscaled
<span class="nb">sudo </span>tailscale up
<span class="c"># Authenticate by using firefox to visit the URL that 'tailscale up' prints out.</span>
<span class="c"># Enable port forwarding.</span>
<span class="nb">echo</span> <span class="s1">'net.ipv4.ip_forward = 1'</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf
<span class="nb">echo</span> <span class="s1">'net.ipv6.conf.all.forwarding = 1'</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf
<span class="nb">sudo </span>sysctl <span class="nt">-p</span> /etc/sysctl.conf
<span class="nb">sudo </span>Tailscale up <span class="nt">--advertise-exit-node</span> <span class="nt">--advertise-routes</span><span class="o">=</span>192.168.86.0/24
<span class="c"># Then go to the Tailscale admin console and turn on the exit node and advertise routes switches for the newly added node.</span>
</code></pre></div></div>
<p>Once this node was active, my son was able to access the Tablo by:</p>
<ol>
<li>Joining his laptop to the family Tailscale network.</li>
<li>Setting the Debian node as his exit node.</li>
<li>Logging into the Tablo web site on his laptop.</li>
<li>Connecting his laptop to the family Tablo device.
This step was the goal of this whole exercise.
This is the step that Tailscale made possible.</li>
<li>Once he had connected once to the family Tablo device, he was able to disconnect from the Tailscale exit node.</li>
<li>At this point, his laptop was able to connect to the family Tablo device over the regular Internet.</li>
<li>At this point, I could turn off the Debian Tailscale node, and return the PC to its Windows configuration.</li>
</ol>
<p>For what it’s worth, once the Tablo app was set up, Tablo streaming worked better over the regular Internet than through the
Debian Tailscale subnet router. Presumably this was because of the overhead of routing packets through the Debian Tailscale subnet router.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Things that went well:</p>
<ul>
<li>The Tailscale free account creation process and signup process was excellent.</li>
<li>The Tailscale Mac and iOS apps were great.</li>
<li>The Tailscale documentation was great.</li>
<li>The Tailscale admin console was amazingly clear and responsive.</li>
<li>The Tailscale app error messages were excellent. They held my hand through the installation process.
(Things like explaining that I needed to enable port forwarding, and giving the commands to use.)</li>
<li>The Debian Live CD recognized all the hardware necessary to connect to the Internet.</li>
<li>Once configured, the Tablo remote connect streaming software worked well.</li>
</ul>
<p>Things that did not go so well:</p>
<ul>
<li>I had to install Linux just to run a Tailscale subnet router.</li>
<li>The Tailscale Debian installation instructions omit the step of running <code class="language-plaintext highlighter-rouge">sudo systemctl start tailscaled</code> after installing Tailscale.</li>
</ul>
<p>Overall my son and I give Tailscale two thumbs up! It enabled us to set up the Tablo DVR application remotely,
so that we could co-watch the Superbowl even though we were 1400 miles apart. Thanks Balena, Debian, Tablo, and Tailscale!</p>Jack PalevichMy son’s away at college, without a TV. He wanted to watch the Superbowl. We realized that one way to do that would be for him to access our family’s Tablo DVR remotely. Tablo supports remote access, but there’s a catch: The client software has to be set up while the Tablo device and the client machine are on the same local network. But my son was 1400 miles away. This seemed like a good opportunity to experiment with a Tailscale private network. And therein lies a tale.The Zen of Advent of Code2021-12-22T00:00:00-08:002021-12-22T00:00:00-08:00https://jackpal.github.io/2021/12/22/The_Zen_of_Advent_of_Code<p>For the past three years I’ve been participating in the
<a href="https://adventofcode.com/">Advent of Code</a> programming puzzle contest. It’s a free contest that
has run every December since 2015. It’s appropriate for people who can write programs at the
undergraduate college student level. (Which means that many high school
students can do it.)</p>
<p>The way the contest works is that, starting at midnight East Coast time on the morning of
December 1st, a puzzle is announced every day from December 1st to December 25th.</p>
<p>The puzzles are designed to be solved by an ordinary developer within a few hours. The focus
tends to be on figuring out a good algorithm, and the problems are usually solvable using under a
hundred lines of code in standard Python.</p>
<!--more-->
<p>Although Python is the focus of the contest, you can use any language you want, and for the most
part the problems can be solved using any language. The typical pitfalls of using a language
besides Python are:</p>
<ul>
<li>
<p>Some problems require processing large integers. You may need to find or implement a “BigNum”
library for your language. (Although usually 64 bit ints are enough.)</p>
</li>
<li>
<p>Some problems are easily solved by using standard data structures (like a priority queue)
that you will have to find or implement for your language.</p>
</li>
</ul>
<p>Each year’s puzzles start out easy, and typically get harder over the days leading up to December
24th. December 25th’s puzzle is usually easy. Each puzzle comes in
two parts. The first part is usually easy, to make sure you understand how to parse the puzzle input and that you understand the general nature of the puzzle. The
second part is usually a twist on the first part, that makes the puzzle harder to solve. Especially in later days, you may find that your solution to the first
part has to be rewritten to solve the second part.</p>
<p>In early years of the contest the puzzles were themed around Christmas. Over the years this focus
has faded, and in recent years Christmas barely appears beyond a few token mentions of “elves”.
I miss the Christmas theme, but I think the puzzle creator ran out of Christmas-related jokes and
ideas.</p>
<p>Anyway, here are my tips for having an enjoyable time participating in the contest:</p>
<ol>
<li>
<p>Depending on where you live, the puzzles may be released the day before their date. For
example, on the West Coast of the US, the first day’s puzzle will become available at
9 pm on November 30th.</p>
</li>
<li>
<p>Don’t worry about your leaderboard position. I myself have never placed higher than 490 on
any part of any problem, but I usually place in the 1000s-3000s, and I still enjoy
participating.</p>
</li>
<li>
<p>You can save a few minutes each day by starting your IDE and creating empty functions and
files for the day’s problems before the contest starts for the day.</p>
</li>
<li>
<p>Read the problem description fully. Make sure you’re solving the problem they ask you to
solve, and not some similar or harder problem. I’ve occasionally wasted hours solving a
general case when the problem only required solving an easier specific version of the problem.</p>
</li>
<li>
<p>If you find yourself writing more than 100 lines of code, or taking more than an hour, you
probably haven’t figured out the optimal way of solving the problem. All the problems so far
have been solvable in normal Python, using normal data structures like list, dictionary, set,
and tuple.</p>
</li>
<li>That being said, a few Python libraries and data structures have proven helpful in many
problems:
<ul>
<li>Counter</li>
<li>heapq</li>
<li>deque</li>
</ul>
</li>
<li>Python-specific advice:
<ul>
<li>Don’t bother with regular expressions. Unless you know regular expression like the back of your hand, it’s almost always faster and easier to parse the
puzzle input using “split” and “int”.</li>
<li>Intentionally avoid programming practices that are good for larger programs, because they will slow you down for tiny programs like these:
<ul>
<li>Don’t parse the input into an intermediate data structure. Instead, parse as you go about solving the puzzle.</li>
<li>For the most part, don’t define subroutines.</li>
<li>For the most part, don’t define classes. You can get really far with tuples, lists, sets and dicts.</li>
<li>Use short variable names.</li>
<li>Use “for” loops instead of comprehensions.</li>
</ul>
</li>
<li>Generally you won’t need numpy or related libraries.
<ul>
<li>The puzzle author is aware of scipy, numpy, networkx, etc, and tends to add “twists” to the puzzle that make it difficult to solve the puzzle simply by
calling a pre-built function of one of these libraries.</li>
</ul>
</li>
<li>Python plotting libraries are useful for making visualizations of your data for debugging and or creating Reddit posts or blog posts.</li>
</ul>
</li>
<li>Swift-specific advice:
<ul>
<li>Swift string processing is verbose. Do yourself a favor and write Swift versions of Python’s “split”, “join”, and enumerate-by-character functions before the
contest starts.</li>
<li>Swift lacks some useful collection classes. Write or find your own version of priority queue and counted set.</li>
<li>Swift’s fancy map/reduce methods work, but simple for loops are often faster and easier to write.</li>
</ul>
</li>
<li>Many years have had problems related to the following topics, so it’s worth reading about them
ahead of time:
<ul>
<li>Chinese Remainder Theorem.</li>
<li>Ring buffers</li>
<li>Searching graphs for optimal routes.</li>
<li>Cellular automata.</li>
<li>One year had many problems related to writing an interpreter for a simple computer. However, this was so difficult for many contestants that I am not sure
we’ll see any more puzzles on this topic.</li>
</ul>
</li>
<li>
<p>The Reddit community <a href="https://www.reddit.com/r/adventofcode/">r/adventofcode</a> is a wonderful
resource of support and ideas related to the contest.</p>
<ul>
<li>If you get stuck on a problem, I recommend visiting the corresponding r/adventofcode solution thread and reading through the highest-rated solutions.
<ul>
<li>There is no shame in doing this. Some of the problems are very difficult to solve if you’re not familiar with a particular obscure algorithm or technique.</li>
</ul>
</li>
</ul>
</li>
<li>Try finding someone to be a contest buddy. In the past two years I’ve been doing the contest
with my kids (who are high school and college age).
It’s been fun discussing the problems and comparing implementations. My son’s recently gotten faster than me at solving the problems. I try to be
philosophical about being surpassed.</li>
</ol>Jack PalevichFor the past three years I’ve been participating in the Advent of Code programming puzzle contest. It’s a free contest that has run every December since 2015. It’s appropriate for people who can write programs at the undergraduate college student level. (Which means that many high school students can do it.) The way the contest works is that, starting at midnight East Coast time on the morning of December 1st, a puzzle is announced every day from December 1st to December 25th. The puzzles are designed to be solved by an ordinary developer within a few hours. The focus tends to be on figuring out a good algorithm, and the problems are usually solvable using under a hundred lines of code in standard Python.Why Apple is Unlikely to Create a Game Console2021-11-27T00:00:00-08:002021-11-27T00:00:00-08:00https://jackpal.github.io/2021/11/27/Apple-console-unlikely<p>Apple’s new M1 Max SOC has good graphics performance. Some people are speculating that this would enable Apple to create a competitive video game console. I think that’s unlikely.</p>
<p>The bull case for Apple making a video game console is:</p>
<ul>
<li>Apple has designed a series of excellent SOCs to serve their existing product lines.</li>
<li>The recent Macbook Pro M1 Max SOC has performance that is comparable to AMD/NVIDIA laptop gaming SOCs, but at lower power consumption.</li>
<li>Apple could use the M1 Max SOC to create a video game console with similar performance to existing video game consoles, but with much lower power requirements.</li>
<li>This would enable Apple to compete in the console video game market.</li>
</ul>
<p>The bear case is:</p>
<ul>
<li>Apple doesn’t care about the console video game market. It’s small and not growing.</li>
<li>Video game console customers don’t value the benefits that Apple’s M1 Max SOC would bring to video games.</li>
</ul>
<p>In this blog post I explain why the video game console market is shaped the way it is.</p>
<!--more-->
<p>Here’s why video game consoles are cheap & hot:</p>
<ul>
<li>People have a wide variety of choices in how they play games. Most people will play the best games available out of the possible choices.</li>
<li>Everyone has a smart phone, with an endless supply of pretty good games. This limits the video game console market to game experiences that are not possible or not enjoyable on phones.</li>
<li>Improving graphics (and audio) is a reliable, effective way of differentiating a console game from a mobile game.</li>
<li>TVs are increasing in resolution every console generation. While TV resolution is in the era of diminishing returns, customers are for the moment still want to buy higher resolution TVs every console generation.</li>
<li>Higher res TVs require 4x the GPU processing power (and RAM) each generation.</li>
<li>As a result, the video game console customer tends to value a console with high CPU and GPU power, lots of RAM.</li>
<li>Using a fixed performance profile (fixed for the life of the console) significantly increases available performance by allowing developers to do all sorts of microoptimizations that would not be feasible if there were differences between consoles.</li>
<li>Video game consoles are in production over 5-7 years. Over that time some of their components may get cheaper (CPU, RAM), others may just get higher capacity at the same price (spinning hard disks). Others may stay the same cost (power supplies). And the CPU & RAM typically switch to better / cooler running processes.</li>
<li>Console vendors can (and do) make yearly updates to the hardware, swapping components to cost reduce the product. This is planned into the console design from the beginning.</li>
<li>Sales increase yearly over the life of the console (as the price is brought down, and as the catalog of games increases and network effects kick in.)</li>
<li>People have a fixed upper bounds on the price they are willing to spend up front, and similar upper bounds on cost-per-game and cost-per-month. This establishes an expected lifetime value of a console purchaser, which sets the limit on bill-of-materials. (Although you can segment the market, like Xbox S / Xbox X.)</li>
</ul>
<p>Those facts together mean you want to design your console so that it’s as powerful as possible for a given price.</p>
<ul>
<li>For a given CPU design and silicon process, you can usually increase performance by increasing the voltage.</li>
<li>Increasing the voltage increases the heat output.</li>
<li>Air cooling remains the cheapest cooling solution.</li>
<li>It’s hard to move a lot of air quietly.</li>
<li>Hardware designers are almost always optimistic about power/heat. Consoles typically end up generating more heat than their designers expected. When that happens, all you can do is crank up the fan.</li>
</ul>
<p>Put all this together, and it’s natural for the first year’s version of a console design to be as hot and noisy as possible. We’ll eventually see consoles top out at a limit of ≈1200 watts, due to US household electricity codes.</p>
<p>Given all this, Apple’s theoretical cool-and-quiet-and-expensive device is going to have to compete against hot-and-loud-and-cheap devices. Most consumers are going to choose the cheap devices.</p>Jack PalevichApple’s new M1 Max SOC has good graphics performance. Some people are speculating that this would enable Apple to create a competitive video game console. I think that’s unlikely. The bull case for Apple making a video game console is: Apple has designed a series of excellent SOCs to serve their existing product lines. The recent Macbook Pro M1 Max SOC has performance that is comparable to AMD/NVIDIA laptop gaming SOCs, but at lower power consumption. Apple could use the M1 Max SOC to create a video game console with similar performance to existing video game consoles, but with much lower power requirements. This would enable Apple to compete in the console video game market. The bear case is: Apple doesn’t care about the console video game market. It’s small and not growing. Video game console customers don’t value the benefits that Apple’s M1 Max SOC would bring to video games. In this blog post I explain why the video game console market is shaped the way it is.