Hyperlink Your Hearthttps://blog.hyperlinkyourheart.com/2023-11-30T22:31:00+01:00Until there's nothing left.I Accidentally Visual Scripting2023-11-30T22:31:00+01:002023-11-30T22:31:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2023-11-30:/accidental-visual-scripting.html<p>I seem to be accidentally implementing a visual scripting system as part of my tooling…</p><p>For some months now I’ve been trying to make some progress on the game I’ve been “working on” for probably the better part of a decade, <a href="https://gamejolt.com/games/just-a-robot/185852" title="Just a Robot on GameJolt">Just a Robot</a> (yep, 7 years ago was the first post there, and it was already in progress for a few years before that!). I haven’t really been working on it for most of that time, though it has always been in the back of my mind.</p>
<p><img alt="Variations on the character art over the years, including sprites and a portrait" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/art_changes.png" title="All I actually seem to do is redesign the art..."></p>
<p>While I have re-implemented some basic <em>gunplay</em> mechanics, and experimented with auto-tiling and the like, my main task has been improving the editor for cutscene graphs that I designed previously (and which I’ve <a href="https://blog.hyperlinkyourheart.com/coroutine-callbacks.html" title="Coroutine Callbacks">mentioned</a> a <a href="https://blog.hyperlinkyourheart.com/all-my-yields.html" title="All My Yields post">few times before</a>) based on my experience of using it in a <a href="https://blog.hyperlinkyourheart.com/gophers-post-mortem.html" title="Gophers Post-mortem">couple</a> of <a href="https://blog.hyperlinkyourheart.com/out-of-gas-post-mortem.html" title="Out of Gas Post Mortem">jam games</a>, and the anticipated requirements for this game.</p>
<p>I am also hoping that it will be something that other people might find useful, and that I can release in the Godot asset library. As such I try to do things in a generalised and user-friendly way, with nothing that would tie it specifically to my game, or rough edges that I would just ignore myself but be embarrassed if other people had to deal with them.</p>
<p>As a result, it’s taking quite a while!</p>
<p>One thing I’ve noticed is that as I try to maintain flexibility I seem to be implementing tiny haphazard visual scripting systems for calculating values and defining conditions. Of course the graph editing is itself a type of visual scripting, but that that was both what I was expecting to be designing, and has well established <span class="caps">UI</span> conventions in the engine. I don’t think there are any conventions or controls for defining values and conditions in the way that I am.</p>
<h2>Variable Changes</h2>
<p>In the initial incarnation of the graph editor, it was possible to set the value of variables, and branch based on the value of variables, but the way it was implemented was… not good.</p>
<ul>
<li>Variable names were entered as strings. This seemed like a problem to me - as the scope of a project grew I anticipated that it would become harder and harder to keep track of what variables were being used, and if they were entered correctly everywhere.</li>
<li>Values were also always strings - if you wanted a boolean, just enter “true” or “false”!</li>
<li>There was no scoping, just one global pool of variables for all graphs.</li>
<li>You could only assign constant values to variables. No incrementing or decrementing, arbitrary calculations, or using the values of other variables.</li>
<li>You could only compare to constant values for branching.</li>
</ul>
<p><img alt="Screenshot of early version of the editor, showing string variable name and value fields" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/dialogue_editor_example.png" title="This Naomi be Wolf"></p>
<p>My initial changes to improve this situation were to introduce scoping and type definitions for variables. Variables could be scoped to the graph, to the area (i.e. “level” or “room” or however the game wanted to define it), or be global. They could also be <code>boolean</code>, <code>int</code>, <code>float</code>, or <code>string</code>. The <span class="caps">UI</span> would change to reflect the type, so you would get a checkbox for booleans and a numbox for numbers.</p>
<p>This was better, but still kind of awful, because everywhere a variable was used the scope and type would have to be selected again! Unlike in code there was no way to declare a variable once and then use it elsewhere with its type and scope already known.</p>
<p><img alt="Screenshot of one of the graphs from my game "Out of Gas", showing scoped boolean variables with values set by checkboxes" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/teenagers_dialogue.png" title="Teenagers up to no good as usual"></p>
<p>So next… I implemented variable declarations, more or less! These are defined in the project, and anywhere that a variable is required in a graph it can be selected from a searchable dialog.</p>
<p><img alt="Dialog for defining a variable" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/variable_definition.png" title="Defining a variable">
<img alt="Variable set node with the variable selection control and a boolean value" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/predefined_variable_set.png" title="Variable Set node">
<img alt="Dialog for finding a variable" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/variable_selection.png" title="Selecting a variable"></p>
<p>Now I felt like I was getting places, though the problem of only being able to set and compare constants remains, and is what I’m currently tackling. But more about that in a minute.</p>
<h2>Choice Conditions</h2>
<p>Another feature of the initial version of the graph editor was the ability to make dialogue choices only conditionally available to the player - for example making a choice dependent on having encountered a particular character or completed a particular quest. This was of course based on the comparison of a variable to a string constant, so it had all the same deficiencies as everything else about the variables, as well as a few of its own:</p>
<ul>
<li>Only one variable could be used for each choice.</li>
<li>The only comparison available was equality, no greater or less than or anything like that.</li>
<li>There was no possibility of negation.</li>
</ul>
<p>The minimal improvement would be to allow a comparison of a single variable with a selectable operator, and a constant value. But what if the choice depends on the value of multiple variables? What if you want to make different choices available if the player has encountered a character but not completed a quest, than if they have done both? This could maybe be achieved using branching nodes to set a third variable, but that seemed like jumping through a lot of unnecessary hoops. I wanted to be able to define complex conditions directly on the choices.</p>
<p>My solution was to add a dialog where an arbitrarily complex condition can be defined - though currently only with constants on the right side of any operator.</p>
<p><img alt="A screenshot of the condition definition dialog alongside the node that it was invoked from" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/farnsworth.png" title="The Farnsworth Condition"></p>
<p>As you can see above, the condition is structured as a tree with boolean operators grouping the results of comparison operators. The whole condition is summarised as its (frankly much more understandable) equivalent GDScript.</p>
<p>So mission mostly accomplished - it was now possible to define conditions on choices with an arbitrary number of clauses. However, it was starting to seem like a lot of <span class="caps">UI</span> complexity to achieve something that is quite simple to do in code - almost like a type of visual scripting…</p>
<h2>Visualising Values</h2>
<p>Now it’s time to tackle the other side of the equation - the values to set or compare the chosen variables to.</p>
<p>For setting variables, I wanted it to be possible to increment or decrement values as well as setting static ones… But why not also allow values to be multiplied or divided? What if you want to set a variable to 2x another variable, or the result of a more complex calculation? These don’t seem like particularly likely requirements for my game, but why reduce the flexibility by only allowing a handful of fixed operations?</p>
<p>For comparisons, I initially only considered allowing a choice between a constant or another variable. But would these not also benefit from more flexibility? What if you want to check if a variable is greater than half another variable? If setting variables was as flexible as described above, this could be achieved by setting a temporary variable, but that seems unnecessarily round-about and annoying to have to do. Since the task is the same for both situations (obtain a value for the right side of an operator), it seemed like it would be prudent to create one control that would cover both.</p>
<p>Another factor is that most of the above concerns only apply to integer and float variables. Booleans have another set of operators that might be applied to them. Strings have a more restricted set of operators, but there are a variety of functions that you might want to apply, or methods that you might want to call on them - <code>to_lower</code>, <code>rstrip</code>, <code>replacen</code>, etc. In fact, the same might be true of integers and floats…</p>
<p>With all that in mind the requirements have become quite complex - I’m faced with implementing a small but significant subset of GDScript in a <span class="caps">GUI</span>!</p>
<p>The design I have so far allows for multiple variables or constants to have operators to be applied between them, grouped by brackets if necessary, and for a selection of appropriate (for the type) functions to be called:</p>
<p><img alt="Screenshot of the design of the proposed value calculation control in the designer" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/value_calculation_mockup.png" title="That's a really long and weird way to write 1..."></p>
<p>One thing that annoys me about this is that it’s much the same structure as is involved in creating the conditions (a tree), but uses completely different controls and looks and works completely differently. It’s going to look quite strange when they are side by side in the conditions dialog… However, the tree control used for the conditions probably suits that better because the elements need to be selectable, and the “prefix notation” also suits it better I think, while it would be unfamiliar for most people for mathematical operators.</p>
<p>I can’t really tell at this point if this is at all intuitive or if there’s an obviously much simpler solution that I’ve overlooked. But it certainly makes me long to be able to just enter the values as code, regardless of their composition.</p>
<h2>Could I Uuuh… Do That?</h2>
<p>I have been thinking about what would be involved in allowing the user to enter conditions and values as the GDScript code they would likely already be familiar with.</p>
<p>Godot includes an <code>Expression</code> class which can be used to parse and execute arbitrary expressions, so conceivably that could be used to run whatever the user input, and make the entire GDScript language available to them. It looks like it can even parse the text for errors before running it, which would be all that I would want to do in the editor anyway. One likely difficulty is that the referenced variables would not actually be GDScript variables at runtime, but entries in one of several dictionaries, so I might have to write my own parser anyway to pick those out and replace them. I’m not sure I really want to do that.</p>
<p>If that didn’t work out, the alternative would be to parse the input myself (even more parsing!) and convert it into the same resource structures that I’m intending to have the <span class="caps">UI</span> create. This would likely be much more limited in which language constructs and operations it could support - and that might cause confusion or frustration for the user.</p>
<p>And, there are reasons why allowing these things to be defined as code might not be a good idea anyway:</p>
<ul>
<li>I don’t really like the idea of having to switch from a <span class="caps">GUI</span> way of doing things to a code way of doing things when the rest of the plugin is very much <span class="caps">GUI</span> driven.</li>
<li>I don’t like the fact that the predefined variables would not be selectable, negating some of their utility. Or at least, to make them selectable I would have to implement some sort of auto-completion on top of everything else, which might be beyond me! </li>
<li>Some people use C# with Godot, and it’s unclear if the <code>Expression</code> class can parse C# - I suspect it can’t.</li>
<li>Using the <code>Expression</code> class would likely allow arbitrary GDScript to be entered and executed during graph processing. That might be too much flexibility! If I want to allow that it will be its own node type.</li>
</ul>
<p>One other <span class="caps">GUI</span> option I can think of would be to allow value calculations and conditions to be defined using their own type of graphs. This might have the advantage of using well established <span class="caps">UI</span> conventions and existing controls. On the other hand, a graph editor is not ideal for defining a tree, and it would probably appear even more unnecessarily complex and confusing for the undoubtedly most common use cases of setting variables to constant values and defining simple conditions based on single variables. It would also not be any less work for me!</p>
<h2>Conclusion</h2>
<p><span class="caps">UI</span> is hard <i class="fas fa-sad-cry body-icon" /> </p>All My Yield()s, Gone!2023-06-23T17:10:00+02:002023-06-23T17:10:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2023-06-23:/all-my-yields.html<p>The removal of yield() in Godot 4 means a new approach is required for some of its use cases.</p><p>In a <a href="https://blog.hyperlinkyourheart.com/coroutine-callbacks.html" title="Coroutine Callbacks post">previous post</a> I described a way to use the state object returned by a <code>yield()</code> call to control the traversal of a graph - specifically, a graph describing a cutscene or dialogue - where some nodes in the graph require waiting on input from the user or some other event before proceeding.</p>
<p>In <a href="https://godotengine.org/" title="The game engine you waited for.">Godot 4</a> the <code>yield</code> function was replaced with the <code>await</code> keyword. This has the same basic purpose: to suspend execution of the current function and return to the caller, to be resumed at a later time. However, it does not return the state object that <code>yield</code> did, so there is no built-in way to resume the function from the caller (that I can see, anyway).</p>
<p>Fortunately, it is not difficult to recreate the functionality. The first thing we need to do is define a very simple class that we can include instances of in the signals from the graph controller:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">ProceedSignal</span><span class="p">:</span>
<span class="n">signal</span> <span class="n">ready_to_proceed</span><span class="p">(</span><span class="n">choice</span><span class="p">)</span>
<span class="n">func</span> <span class="n">proceed</span><span class="p">(</span><span class="n">choice</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">):</span>
<span class="n">ready_to_proceed</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="n">choice</span><span class="p">)</span>
</code></pre></div>
<p>This class includes a signal and a method that the consumer of the graph can call to tell the graph controller that it can proceed to the next node - similar to the <code>resume()</code> method on the coroutine state object in Godot 3.</p>
<p>The <code>_await_response</code> function which previously yielded to create a resumable coroutine state, now just returns a new instance of this class. It could alternatively just be created directly where this function is called:</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_await_response</span><span class="p">():</span>
<span class="k">return</span> <span class="n">ProceedSignal</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
</code></pre></div>
<p>In <code>process_cutscene()</code> we now <code>await</code> calls to process node types that require waiting on the consumer, rather than <code>yield</code>ing them:</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">process_cutscene</span><span class="p">(</span><span class="n">cutscene</span><span class="p">):</span>
<span class="n">_graph_stack</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">_local_store</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">_current_graph</span> <span class="o">=</span> <span class="n">cutscene</span>
<span class="n">_current_node</span> <span class="o">=</span> <span class="n">_current_graph</span><span class="o">.</span><span class="n">root_node</span>
<span class="o">...</span>
<span class="k">while</span> <span class="n">_current_node</span> <span class="o">!=</span> <span class="n">null</span><span class="p">:</span>
<span class="k">if</span> <span class="n">_current_node</span> <span class="ow">is</span> <span class="n">DialogueTextNode</span><span class="p">:</span>
<span class="k">await</span> <span class="n">_process_dialogue_node</span><span class="p">()</span>
<span class="k">elif</span> <span class="n">_current_node</span> <span class="ow">is</span> <span class="n">BranchNode</span><span class="p">:</span>
<span class="n">_process_branch_node</span><span class="p">()</span>
<span class="o">...</span>
</code></pre></div>
<p>And when processing such a node, we just create the <code>ProceedSignal</code> object, emit the relevant signal with it, and then await the <code>ready_to_proceed</code> signal from it:</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_process_dialogue_node</span><span class="p">():</span>
<span class="o">...</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">text</span>
<span class="n">var</span> <span class="n">character_name</span> <span class="o">=</span> <span class="n">null</span>
<span class="n">var</span> <span class="n">variant_name</span> <span class="o">=</span> <span class="n">null</span>
<span class="k">if</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">character</span> <span class="o">!=</span> <span class="n">null</span><span class="p">:</span>
<span class="n">character_name</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">character</span><span class="o">.</span><span class="n">character_name</span>
<span class="k">if</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">character_variant</span> <span class="o">!=</span> <span class="n">null</span><span class="p">:</span>
<span class="n">variant_name</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">character_variant</span><span class="o">.</span><span class="n">variant_name</span>
<span class="n">var</span> <span class="n">process</span> <span class="o">=</span> <span class="n">_await_response</span><span class="p">()</span>
<span class="n">call_deferred</span><span class="p">(</span>
<span class="s2">"_emit_dialogue_signal"</span><span class="p">,</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">character_name</span><span class="p">,</span>
<span class="n">variant_name</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">)</span>
<span class="k">await</span> <span class="n">process</span><span class="o">.</span><span class="n">ready_to_proceed</span>
<span class="n">_current_node</span> <span class="o">=</span> <span class="n">_get_node_by_id</span><span class="p">(</span><span class="n">_current_node</span><span class="o">.</span><span class="n">next</span><span class="p">)</span>
</code></pre></div>
<p>Nothing much changes from the consumer’s point of view, it just needs to store the object and then call the <code>proceed()</code> method when it’s ready:</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_on_cutscene_controller_dialogue_display_requested</span><span class="p">(</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">character_name</span><span class="p">,</span>
<span class="n">character_variant</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">):</span>
<span class="c1"># Hang on to the process object so we can tell the cutscene controller</span>
<span class="c1"># to continue when we're ready to proceed</span>
<span class="n">_current_process</span> <span class="o">=</span> <span class="n">process</span>
<span class="o">...</span>
<span class="n">func</span> <span class="n">_on_dialogue_display_continue_clicked</span><span class="p">():</span>
<span class="n">DialogueDisplay</span><span class="o">.</span><span class="n">hide</span><span class="p">()</span>
<span class="n">_current_process</span><span class="o">.</span><span class="n">proceed</span><span class="p">()</span>
</code></pre></div>
<p>That’s all the changes required for this project! Of course, the coroutine state object also had a property indicating if it was resumable or not, <code>is_valid</code>. It would not be difficult at all to reproduce this by simply adding such a property (perhaps behind a setter that would make it read-only except internally), and setting it to false once <code>proceed()</code> is called.</p>
<p>It could also be expanded to allow more complex communication between the coroutine and the consumer, or to make a long running coroutine cancellable. The controller can only <code>await</code> one type of signal to continue, but you could have it pass different instructions when resuming e.g. you could give it <code>stop()</code> and <code>proceed()</code> methods. Additional properties on the signal object could be used to pass back other data without having to pass it in the signal at all.</p>
<div class="highlight"><pre><span></span><code><span class="n">enum</span> <span class="n">ProceedSignalType</span> <span class="p">{</span>
<span class="n">STOP</span><span class="p">,</span>
<span class="n">PROCEED</span>
<span class="p">}</span>
<span class="k">class</span> <span class="nc">ProceedSignal</span><span class="p">:</span>
<span class="n">signal</span> <span class="n">ready_to_proceed</span><span class="p">(</span><span class="n">signal_type</span><span class="p">)</span>
<span class="n">var</span> <span class="n">consumer_state</span>
<span class="n">func</span> <span class="n">stop</span><span class="p">():</span>
<span class="n">ready_to_proceed</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="n">ProceedSignalType</span><span class="o">.</span><span class="n">STOP</span><span class="p">)</span>
<span class="n">func</span> <span class="n">proceed</span><span class="p">():</span>
<span class="n">ready_to_proceed</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="n">ProceedSignalType</span><span class="o">.</span><span class="n">PROCEED</span><span class="p">)</span>
</code></pre></div>
<h2>Cutscene Graph Editor Status</h2>
<p>The cutscene graph editor has been upgraded to support Godot 4, at a <a href="https://github.com/khoulihan/godot4-cutscene-graph-editor" title="Cutscene Graph Editor project on GitHub">new home</a>. I’ve also added a bunch of minor features, such as multi-node deletion, copy <span class="amp">&</span> paste and duplication support, and support for dragging from an output port to create a new node.</p>
<p>I’m now working on improving some parts of the tool that were lacking in flexibility. My current task is improving the addition of conditions to the choice and random nodes, which previously only allowed a single variable to be compared for equality to determine if a branch should be considered. The new system moves the condition specification <span class="caps">UI</span> out of the nodes themselves and into a dialog box, and allows any number of variables to be evaluated using a variety of different operators.</p>
<p><img alt="The Farnsworth Condition" src="https://blog.hyperlinkyourheart.com/images/all-my-yields/graph.png" title="The Farnsworth Condition"></p>
<p>Future plans include new ways of defining and interacting with sub-graphs, more flexible ways of manipulating variables, built-in variables and meta-data, and better ways of defining characters.</p>
<h2>Update: Pure Signals</h2>
<p>It occurred to me after posting this that there is an even easier way to achieve this - as long as you don’t need to keep any state in the signalling object. Because Godot 4 allows you to pass signals and callables, you could just pass the signal to proceed with the signal that initiates the action. When the consumer is ready to proceed they can then just call <code>emit</code> on it.</p>
<p>I might still prefer the other way of doing it because <code>_current_process.proceed()</code> reads a little better than <code>_current_process_proceed_signal.emit()</code>.</p>A Culture of Conspiracy2023-05-21T17:56:00+02:002023-05-21T23:08:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2023-05-21:/culture-of-conspiracy.html<p>A book about nonsense.</p><p>I read <a href="https://www.amazon.com/Culture-Conspiracy-Apocalyptic-Contemporary-Comparative-ebook/dp/B00DNJD46C/ref=sr_1_1?dchild=1&keywords=a+culture+of+conspiracy&qid=1604350628&sr=8-1" title="A Culture of Conspiracy on Amazon">“A Culture of Conspiracy”</a> by Professor Michael Barkun a few years ago. In it, he describes several broad categories of conspiracist belief, and traces the development of a variety of beliefs from their origins through to the time of writing as they branch and mutate and recombine.</p>
<p>I started this post not too long after reading it but I’ve let it languish for several years now. I hope I’m not misrepresenting its contents due to my failing memory, but I would recommend you read it yourself and find out - it’s definitely worthwhile, and I’m only picking out a few bits of it to talk about that were most interesting to me personally.</p>
<h2>Conspiracy Types</h2>
<p>Barkun identifies three types of conspiracy theories based on their scope:</p>
<ol>
<li><strong>Event conspiracies</strong> - limited to a specific event, such as the assassination of <span class="caps">JFK</span>.</li>
<li><strong>Systemic conspiracies</strong> - concern the plans of a specific organisation or group with broad goals, such as taking over the world.</li>
<li><strong>Superconspiracies</strong> - this type of conspiracy links together various other conspiracies of the <em>event</em> and <em>systemic</em> varieties in a hierarchical manner (e.g. the <span class="caps">CIA</span> assassinated <span class="caps">JFK</span>, but the <span class="caps">CIA</span> are a tool of the Illuminati and his assassination served their purposes, but <em>really</em> the Illuminati are in thrall to Satan etc.)</li>
</ol>
<p>Mostly this makes me wonder if systemic or event conspiracies ever really exist on their own in anybody’s mind anymore, because they all seem to espouse and nod along with such a mish-mash of ideas. He does note that superconspiracies have been on the rise since the 1980s. But is something like QAnon even separable from the web of conspiracist ideas in which it seems to be embedded?</p>
<h2>Stigmatised Knowledge</h2>
<p>Barkun identifies the origin of conspiracist thinking in <em>stigmatised knowledge</em>:</p>
<blockquote>
<p>By stigmatized knowledge I mean claims to truth that the claimants regard as verified despite the marginalization of those claims by the institutions that conventionally distinguish between knowledge and error.</p>
</blockquote>
<p>Stigmatised knowledge is not exclusive to conspiracism, but it is inevitably a feature of it, and I think it leads to it readily even when it initially exists without it. For example a believer in a discredited alternative medical treatment might not initially believe in any specific conspiracy in connection to it, but eventually they will have to explain why it is not accepted into the mainstream, and an easy answer is that it is being suppressed by a conspiracy of insiders who benefit from it not being adopted.</p>
<p>With this concept in mind it is easy to understand the origin of “pipelines” into conspiracism from any pseudoscientific field, fringe religious movement, or even from political ideologies that see their righteousness as obvious and their victory as inevitable. When their distorted worldview meets reality and they have to explain their failures, conspiracism is right there. Once they’re understanding one piece of stigmatised knowledge as being suppressed by a conspiracy, it’s a small step to accepting other such claims.</p>
<h2>Fact is Fiction, Fiction is Fact</h2>
<blockquote>
<p>The commonsense distinction between fact and fiction melts away in the conspiracist world. More than that, the two exchange places, so that in striking ways conspiracists often claim first that what the world at large regards as fact is actually fiction, and second that what seems to be fiction is really fact.</p>
</blockquote>
<p>Anybody who has spent any time listening to conspiracists will recognise the truth of this statement right away.</p>
<p>First of all, all conspiracy theories necessarily involve claims that one or more generally accepted truths are actually lies intended to pacify or deceive the sheep. Often any information coming from an institution is dismissed without consideration, because it is a given that institutions - be they governments, universities or the “mainstream media” - are “in on it”.</p>
<p>With fact rendered fictional, it becomes very easy to point to fiction to fill gaps in their evidence. Sometimes this takes the form of taking fictional sources as literal accounts, and Barkun describes some of these instances. Other times fictional stories are said to contain encoded messages or to be for the purpose of softening up the masses to accept some coming revelation or societal change - nothing can ever be somebody’s neat idea for a sci-fi concept, or allegory, or their opinion of where society is or is going, unplanned.</p>
<p>In some accounts, I believe that describing their literal plans in fiction is believed to have an occult purpose for the conspirators, much like is claimed about symbols on currency or on buildings (e.g. Denver airport). Advertising their plans in a way that will only be understood by an enlightened few is somehow a part of bringing them to fruition. This is how Alex Jones is interpreting H.G Wells’ “Time Machine” when he talks about Eloi and Morlocks, saying “it’s all right there folks” - and similarly for the other pop cultural works he references, of course.</p>
<h2>Emergency Management</h2>
<p>One of the things I was most eager to learn about from this book was <span class="caps">FEMA</span> camp conspiracy theories. I find these theories amongst the most frustrating (and amusing) because of how they look past the very real historical precedents of concentration camps, and the present day realities of mass incarceration and political repression in the United States and elsewhere, and focus instead on a long running conspiracy that is always just on the cusp of rounding up those troublesome “patriots”.</p>
<p>Of course, the longer this conspiracy has been in the milieu the more absurd it becomes. Barkun identifies the origins of this theory as a pamphlet by a man named William Pabst, written sometime prior to 1979. Pabst warns: “your country and way of life [will be] replaced by a system in which you will be a slave in a concentration camp”.</p>
<p>As such, more recent incarnations of this theory imply that the <span class="caps">US</span> government (acting on behalf of some hidden puppet-masters, perhaps) has been building and maintaining a network of secret camps for <em>over 40 years</em> without ever putting their nefarious plans into motion!</p>
<p>Historical instances of the use of internment and concentration camps by governments are of course very real. However, they have never required such extensive periods of preparation. When the British government decided to round up Irish Nationalists in Northern Ireland, they built temporary structures in the weeks prior to doing so, and more permanent structures over the next few years after that. The United States forced 120,000 Japanese Americans into camps during <span class="caps">WWII</span>, first in hastily converted racetracks and fairgrounds, and then in more permanent facilities built over a few months in 1942. Even the horrifying machinery of the Nazis did not require decades to construct, instead comprising a mix of repurposed buildings of many types, and camps newly constructed during the course of the war - a system that imprisoned and exterminated millions.</p>
<h2>The Speed of Lies</h2>
<p>When this book was first published in 2003 it had already been updated from the largely completed manuscript to include chapters concerning the explosion of conspiracism after the 9/11 attacks. The second edition, published in 2013, which is the one I read, had been updated with chapters about birtherism and millenarian conspiracies about the year 2012.</p>
<p>In a testament to the veracity of the saying that “a lie can travel half-way around the world while the truth is putting its shoes on”, many of the conspiracies considered in the book, even the later additions, seem quaint and out-of-date from the vantage point of 2023. Of course any book on the constantly shifting, slippery world of conspiracism will be out-of-date (in some ways) within a few years of coming out.</p>
<p>Nonetheless, the analysis is still useful to understanding the process of the creation and dissemination of conspiracist ideas. Indeed there is no amount of time and lack of confirmation that will kill many conspiracies - the reason I was so focused on <span class="caps">FEMA</span> camp conspiracies in this post was because somebody told me just a few years ago that Hillary Clinton would have put everybody in camps, and similar rhetoric arose even more recently when the language around <span class="caps">COVID</span>-19 mitigation measures was claimed to be intended to “make us feel like we’re in prison” - a <em><span class="caps">FEMA</span> camp of the mind</em> I guess.</p>
<p>The only writing of Barkun’s that I’ve read concerning more recent developments in the conspiracy sphere is an <a href="https://foreignpolicy.com/2018/11/08/failed-prophecies-wont-stop-trumps-true-believers/" title="Michael Barkun on QAnon">article in “Foreign Policy”</a> about QAnon which examines the efforts of its adherents to cope with its failed prophecies. As far as I’m aware QAnon is still going strong despite its predictive failures.</p>
<p>QAnon has been described by some as a “big tent” conspiracy theory because of its ability to adapt and incorporate new claims. However, it’s hardly unique in that regard - <span class="caps">NWO</span> conspiracy theories and many others have been interpreting events through their particular lenses and adapting and incorporating new claims for decades. QAnon might be unique in terms of its longevity <em>despite having made specific, dated predictions that failed to come to pass</em>, but to me it seems more like a systemic conspiracy that conspiracists have been rolling into their own long-existing superconspiracies. It only seems like QAnon is the “big tent” because it broke so spectacularly into the mainstream. As it breaks down under the weight of its failures it seems like it is adapting to include other theories, but it is actually the other theories that are absorbing it into themselves and trying to salvage the parts of it that are useful.</p>
<h3>More Conspiracism</h3>
<p>I started listening to the <a href="https://knowledgefight.libsyn.com/" title="Knowledge Fight Podcast">Knowledge Fight</a> podcast during Alex Jones’ defamation trials to get the scoop on developments, and I haven’t been able to stop listening since. Dan and Jordan’s analysis of Jones’ bullshit is excellent, and it’s a great way of keeping up with what he’s saying. It’s also incredibly entertaining.</p>
<p>I have been meaning to check out the <a href="https://soundcloud.com/qanonanonymous" title="QAnon Anonymous Podcast">QAnon Anonymous podcast</a> as well for a while to get a more general view, but I haven’t gotten around to it yet.</p>Syncthing Update2023-04-08T23:15:00+02:002023-04-08T23:15:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2023-04-08:/syncthing.html<p>Joplin sync didn’t work out long term, but I’m still using Syncthing</p><p>In a <a href="https://blog.hyperlinkyourheart.com/joplin.html" title="Previous Joplin & Syncthing post">previous instalment</a>, I described how I used <a href="https://syncthing.net/" title="Syncthing website">Syncthing</a> to sync my notes in <a href="https://joplinapp.org/" title="Joplin's website">Joplin</a> on my laptop to Joplin on my phone. Unfortunately that arrangement didn’t last long - updates to different versions of Joplin on different devices resulted in incompatible versions of the notes database being synced, and at one point the Android version became unable to export to the filesystem at all. I never got to the bottom of that, but I had moved to using <a href="https://logseq.com/" title="Logseq website">Logseq</a> for most of my notes on the computer anyway, so it didn’t really matter much.</p>
<p>I also mostly fell out of using Syncthing, since I no longer required it for its primary purpose. However I got a Framework laptop recently and had the need to sync my Logseq graphs to it, as well as my music collection, work files, etc. It was such a joy to get it set up and watch files start to zip across to the new machine that I once again had to sing the praises of this amazing piece of software.</p>
<p>Some shares I have set up so far:</p>
<ul>
<li><strong>The camera roll on my phone</strong> - send only so that remote devices can’t add or delete photos. It’s great to have photos zip across to whichever computer I’m using without having to involve Google Photos.</li>
<li><strong>The Default sync folder</strong> - why not? If some random file is needed everywhere it can go in there. It always throws me a bit that these create a common shared folder rather than each device’s folder being it’s own thing, but it’s cool, it’s fine, I’ll get used to it.</li>
<li><strong>My Logseq graph folders</strong> - I set these up to backup any changes just in case, because Syncthing will not perform merges. However I’m not really that worried about it because I will only work on one machine at a time, and if the sync runs regularly it shouldn’t be a problem.</li>
<li><strong>My music collection</strong> - I set this up to ignore <code>*.zip</code> and <code>*.part</code> for uh… reasons. One of the nuisances of a collection of downloaded music is ensuring that every new acquisition gets to every device where you might want to listen to it <em>before</em> you want to listen to it. Well, problem solved! And no more shaming from Spotify for streaming the same album on repeat for several months! (It was Manu Chao)</li>
<li><strong>Several shares specifically between two particular devices</strong> e.g. huge desktop replacement laptop to the Framework, for when I want to share files specifically between those but not my phone.</li>
</ul>
<p>I’m considering syncing some specific work folders as well so I can more easily untether from my desk, but… haven’t decided yet.</p>The Ancaps2022-09-03T18:26:00+02:002022-09-03T18:26:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2022-09-03:/the-ancaps.html<p><span class="caps">HBO</span>’s “The Anarchists” is not really about Anarchists</p><p><img alt="Screenshot of a couple embracing in front of a bonfire, into which anarcho-capitalists are throwing books produced by a government" src="https://blog.hyperlinkyourheart.com/images/the-ancaps/bookburning.jpg" title="What if we kissed at the Anarchist book burning?"></p>
<p><a href="https://www.vice.com/en/article/m7gvdp/real-anarchists-react-to-the-anarchists-a-new-series-about-crypto-bros">Much has been said already</a> about the fact that <span class="caps">HBO</span>’s documentary series “The Anarchists” is not really about anarchists, and by <a href="https://twitter.com/magpiekilljoy/status/1547566126174941186">people far more capable of making the argument</a> than I. Nonetheless, I do have some thoughts on that and other aspects of the documentary.</p>
<p>My overall impression of the documentary is that it is philosophically vacuous and insincere. “Anarchism” is defined superficially by the characters in what is essentially an examination of interpersonal drama. The history of Anarchism proper, and its inherent conflict with capitalism is not explored, but neither, really, is “anarcho”-capitalism or the ideas behind it, <a href="https://tommullentalksfreedom.com/featured/where-is-the-anarchism-in-hbos-the-anarchists/">on their own terms</a> or otherwise. The community is simply mined for drama and spectacle. The main propaganda points of the doc lie in the fact that “freedom” is just implicitly associated with laissez-faire capitalism, and the appropriation of the word “Anarchism” and anarchist symbols by the right, <a href="https://www.goodreads.com/quotes/3194162-one-gratifying-aspect-of-our-rise-to-some-prominence-is">a long-running project</a>.</p>
<h2>The Inherent Contradictions of Ancapitalism</h2>
<p>Though the documentary has little interest in examining them, the cracks and contradictions in the ideology do show through.</p>
<p>I think it is safe to say that everybody who enthusiastically embraces an extreme capitalist ideology thinks that they are, or will be, the boss of whatever enterprise they are involved in. Of course this produces tension when it turns out that somebody’s property rights, and a lack of any critique of property or the hierarchies it produces, makes subordinates of people who consider themselves entitled to be in charge.</p>
<p>The Anarchapulco conference that the documentary focuses on was not organised in a non-hierarchical manner from its conception because anarcho-capitalism does not renounce all hierarchies, only the existence of the state. <a href="https://itsgoingdown.org/it-looks-like-hitler-was-pretty-good-hbo/">Jeff Berwick</a>, the founder and apparent “owner” of the conference behaves throughout as if organising the conference is something that an employee should be doing on his behalf, with his own role limited to giving a keynote, receiving adulation, and partying.</p>
<p>The first such employee we are introduced to is Nathan Freeman, who apparently had a leading role in organising the conference for several years after attending the initial one. It seems to me that Freeman thought himself and Berwick were partners in the endeavour. Berwick obviously saw things differently, and replaced Freeman in 2019 with an outsider.</p>
<p>Tragically, it seems like Freeman couldn’t cope with this humiliation, and essentially drank himself to death. Berwick didn’t even offer condolences to his family, because he’s an enormous piece of shit.</p>
<p>There are a number of aspects to the circumstances of his death that a documentary that was an honest examination of anarcho-capitalism would interrogate. He fell victim to a crypto scam shortly before becoming sick. What is the anarcho-capitalist perspective on this kind of crime? What does history tell us about private money and its effects on society? It’s glossed over as an unfortunate, unavoidable risk of “freedom”. He had no insurance, and his family had to rely on charity to pay his medical bills. What is the anarcho-capitalist perspective on the provision of healthcare? The question is not even asked.</p>
<p>Interestingly, John and Lily, the young couple who flee to Mexico after being arrested on drug traficking charges, do form a critique of the hierarchical, commercial nature of the Anarchapulco conference, and start their own alternative conference called Anarchaforko. It’s a bit unclear the extent to which this is organised at all rather than just people showing up and doing whatever, but it seems to work, and I would love to hear more about how this fits with their apparent objectivist leanings. But of course we get nothing like that.</p>
<p><img alt="Screenshot of Lily Forester post on Facebook: "This conference was supposed to be for ancaps by ancaps!"" src="https://blog.hyperlinkyourheart.com/images/the-ancaps/forancaps.jpg" title="Well that's yer problem right there..."></p>
<h2>Stateless in Mexico</h2>
<p>Probably the funniest aspect of the documentary for me is that the participants seem to think that Mexico is “more anarchist” than the <span class="caps">US</span>, just based on the general vibes. Mexico, of course, does have a state, and I don’t have any reason to think that it is “less of” a state than that of the <span class="caps">US</span>.</p>
<p>I think this sense of “anarchiness” is probably the result of a few different factors. Many of the ancap immigrants are relatively wealthy, and apparently speak little or no Spanish. They are essentially just squatting on top of Mexican society, with no real connections to it, and using their wealth to extract what they need from it. The Mexican state protects them, as states generally protect the wealthy. They have little negative contact with it, and don’t hear about other people’s negative interactions with it as they would in the <span class="caps">US</span>, because they don’t speak the language. They’re just living in a little fantasy colonialist bubble.</p>
<p>Some members of the community are not so well off, and they do have negative experiences with the Mexican state, ranging from dealing with bureaucracy to being pursued, threatened and arrested by the police.</p>
<p>Although Lily Forester is a member of the latter group, it is her concluding statement on the existence of the state that best sums up the general attitude:</p>
<blockquote>
<p>I just want to be left alone, like, a state can exist if it’s going to leave me alone.</p>
</blockquote>
<p>On a personal level I can relate, especially given what she went through, but it’s a far cry from the moral clarity of this Fannie Lou Hamer quote:</p>
<blockquote>
<p>Nobody’s free until everybody’s free.</p>
</blockquote>
<p>Any meaningful conception of freedom can’t ignore that other people are subject to repression or exploitation, but that is exactly what these ancaps constantly do - the Mexican state is fine because I’m a rich foreigner and it leaves me alone, capitalist hierarchies are fine because I’m on top of them.</p>
<h2>M’Aidez!</h2>
<p>As I mentioned above, the primary participants in the documentary fall roughly into two groups - one comprised of relatively wealthy entrepeneurs like Berwick and the Freemans, and the other of struggling working class people like Lily Forester and John Galton, Jason Henza, and Paul Propert.</p>
<p>Though both groups are motivated by more-or-less the same ideology (<a href="https://twitter.com/jasonhenza/status/1559305420816220160">Henza claims himself and Forester are not ancaps</a>, but I don’t really see much distinction between anarcho-capitalism and voluntaryism or agorism myself), the differences between their circumstances is stark. The wealthy run their businesses from their lavish properties while the rest do odd jobs, deal drugs, and otherwise hustle to survive while living in marginal circumstances. As <a href="https://bennorton.com/thaddeus-russel-s-right-wing-libertarian-historical-revisionism/">Thaddeus Russell</a> notes:</p>
<blockquote>
<p>It’s very easy to escape governments, banks and states if you’re already a Bitcoin millionaire. If you’re like John and Lily, you’ve got no resources, nothing, it’s hard, it turns out, and dangerous, in fact, to be an anarchist in Mexico.</p>
</blockquote>
<p>The tension between these two groups is discussed at several points. The drug dealing and other illegal activity (like the theft of a Bitcoin <span class="caps">ATM</span>) are an inconvenience for the wealthy, and the unhinged Paul Propert is a potentially deadly threat to everybody, but they have no solutions. Everybody is just on their own to fend for themselves.</p>
<p>The documentary explores the backgrounds of Galton, Forester and Propert in some detail and finds a variety of broken homes, substance abuse problems, and other traumas. Like the characters themselves, it doesn’t seem to consider for a moment that the source of these traumas is the very social system that they cling to so tightly.</p>
<p>Nonetheless the clearest critique the documentary has for the anarcho-capitalist project is the lack of solidarity and support that those lacking means, and in dangerous circumstances, receive from the community, and what this would imply for an anarcho-capitalist society. Erika Harris, who ends up feeling alienated from the community and leaving Acapulco for Belize, makes this plea for mutual aid after John Galton is murdered, and Lily and Jason are on the run:</p>
<blockquote>
<p>There’s an emergency among us, how will we respond? With shelter, with safehouses, with passage over borders if necessary … We need each other to get this done. I mean, we need each other just to move one inch forward.</p>
</blockquote>
<p>Unfortunately, her plea seems to have fallen on deaf ears.</p>
<p><img alt="Jeff Berwick setting a printout of an American flag on fire with a 100 Bolivar note" src="https://blog.hyperlinkyourheart.com/images/the-ancaps/bolivars.jpg" title="Vuvuzela iPhone Death to America"></p>Nimpressions2022-05-26T20:16:00+02:002022-05-26T20:16:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2022-05-26:/nimpressions.html<p>First impressions of the Nim programming language</p><p>Python is my go-to language for personal projects, and even client projects when I can get away with it (though usually those are Windows based and within the .Net ecosystem, so I stick with C#). However, it often gives me pause to be using one of the slowest and <a href="https://thenewstack.io/which-programming-languages-use-the-least-electricity/" title="Programming languages energy efficiency">least energy efficient</a> languages available - I might do another post about that, but suffice it to say that it doesn’t align with my values to needlessly waste resources.</p>
<p>The ideal would be a language that’s as easy to write as Python, but as fast and energy efficient as C, or close to it. Well recently I came across a language that claims be both of those things: <a href="https://nim-lang.org/" title="Nim homepage">Nim</a>.</p>
<p>I put together a simple command line application (named <a href="https://github.com/khoulihan/luz" title="Luz on GitHub">Luz</a>) in Nim this week in order to try it out. Appropriately enough given my reason for trying Nim, it just shows the current electricity rate band, and optionally a chart, because where I live there are two peak periods during the day when it is better not to do anything power-intensive. I went on to make a start on a very simple Gemini server called Sparkle, which is still a <span class="caps">WIP</span>. Here are some of my thoughts on the experience as a mediocre developer with some Python and C# experience.</p>
<p><a href="https://github.com/khoulihan/luz" title="Luz on GitHub"><img alt="Luz in action" src="https://blog.hyperlinkyourheart.com/images/nimpressions/luz_screenshot.png" title="Going from bad to worse"></a></p>
<h2>choosenim</h2>
<p>Nim has a <a href="https://github.com/dom96/choosenim" title="choosenim on GitHub">tool for installing its toolchain</a> and and switching between different versions of the compiler, similar to pyenv. Unfortunately it didn’t work for me on Pop! <span class="caps">OS</span> 22.04 due to it having too new a version of libssl. I was able to install the Nim compiler manually easily enough by just downloading the tarball and copying the contents to an appropriate location, and then adding the <code>bin</code> directory to my path. There was an install script in the tarball but it didn’t copy everything for some reason.</p>
<p>Not a great start, and I’m not sure what I’m missing out on by not using choosenim, but I can figure that out later if I continue using the language.</p>
<h2>Typing</h2>
<p>Static typing is something I’m well used to from C# of course, but I don’t engage with Python’s type hinting at all. There is type inference in many situations, and many familiar collection types such as sets, tables, sequences and tuples which are as convenient to instantiate as their Python equivalents, though of course you can’t mix unrelated types within them (aside from tuples)(and why would you do that anyway, you monster). Mostly it is just convenient to know at compile time where there are type mismatches, rather than hearing about them at runtime or just getting weird behaviour.</p>
<p>Nim is only very minimally object-oriented. There is inheritance, but not multiple-inheritance, mixins, or anything resembling the interfaces or traits of other languages. This is probably one of the most concerning aspects of the language for me. It seems like it will inevitably lead to repeated code at some point if procedures can’t accept abstract interfaces as input instead of concrete types.</p>
<p>On the other hand I try to steer away from an object-oriented style in Python unless it really makes sense for the problem I’m working on. In Luz, the classes I created were little more than structs, with no inheritance required, and that’s perfectly sufficient for many problems.</p>
<p>There are also apparently <a href="https://github.com/yglukhov/iface" title="iface library on GitHub">libraries</a> that create a means to specify interfaces using meta-programming, but that’s not something I’ve explored yet.</p>
<div class="highlight"><pre><span></span><code><span class="k">type</span>
<span class="w"> </span><span class="n">Holiday</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ref</span><span class="w"> </span><span class="k">object</span>
<span class="w"> </span><span class="n">date</span><span class="p">:</span><span class="w"> </span><span class="n">DateTime</span>
<span class="w"> </span><span class="n">localName</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span>
<span class="w"> </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span>
<span class="w"> </span><span class="n">countryCode</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span>
<span class="w"> </span><span class="n">fixed</span><span class="p">:</span><span class="w"> </span><span class="nb">bool</span>
<span class="w"> </span><span class="n">global</span><span class="p">:</span><span class="w"> </span><span class="nb">bool</span>
<span class="w"> </span><span class="n">counties</span><span class="p">:</span><span class="w"> </span><span class="n">Option</span><span class="o">[</span><span class="nb">seq</span><span class="o">[</span><span class="nb">string</span><span class="o">]]</span>
<span class="w"> </span><span class="n">launchYear</span><span class="p">:</span><span class="w"> </span><span class="n">Option</span><span class="o">[</span><span class="nb">int</span><span class="o">]</span>
<span class="kd">var</span><span class="w"> </span><span class="n">holidays</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">initTable</span><span class="o">[</span><span class="nb">int</span><span class="p">,</span><span class="w"> </span><span class="nb">seq</span><span class="o">[</span><span class="n">Holiday</span><span class="o">]]</span><span class="p">()</span>
<span class="k">proc</span><span class="w"> </span><span class="nf">isHoliday</span><span class="o">*</span><span class="p">(</span><span class="n">d</span><span class="p">:</span><span class="w"> </span><span class="n">DateTime</span><span class="p">):</span><span class="w"> </span><span class="nb">bool</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kp">false</span>
<span class="w"> </span><span class="c"># This will occur if API key was not provided</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">holidays</span><span class="p">.</span><span class="n">hasKey</span><span class="p">(</span><span class="n">d</span><span class="p">.</span><span class="n">year</span><span class="p">):</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">result</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">h</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">holidays</span><span class="o">[</span><span class="n">d</span><span class="p">.</span><span class="n">year</span><span class="o">]</span><span class="p">:</span>
<span class="w"> </span><span class="c"># global indicates that the holiday applies to the whole country</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">h</span><span class="p">.</span><span class="n">global</span><span class="p">:</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">h</span><span class="p">.</span><span class="n">date</span><span class="p">.</span><span class="n">yearday</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">d</span><span class="p">.</span><span class="n">yearday</span><span class="p">:</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kp">true</span>
<span class="w"> </span><span class="k">break</span>
</code></pre></div>
<h2>Uniform Function Call Syntax</h2>
<p>This is really neat - any procedure or function can be called as if it is a method of the type of its first parameter.</p>
<div class="highlight"><pre><span></span><code><span class="k">proc</span><span class="w"> </span><span class="nf">sendErrorResponse</span><span class="p">(</span>
<span class="w"> </span><span class="n">requestSocket</span><span class="p">:</span><span class="w"> </span><span class="n">AsyncSocket</span><span class="p">,</span>
<span class="w"> </span><span class="n">code</span><span class="p">:</span><span class="w"> </span><span class="n">StatusCode</span><span class="p">,</span>
<span class="w"> </span><span class="n">meta</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span>
<span class="p">)</span><span class="w"> </span><span class="sx">{.async.}</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">await</span><span class="w"> </span><span class="n">requestSocket</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="o">&</span><span class="s">"{ord(code)} {meta}</span><span class="se">\r\L</span><span class="s">"</span><span class="p">)</span>
<span class="k">proc</span><span class="w"> </span><span class="nf">processRequest</span><span class="p">(</span><span class="n">requestSocket</span><span class="p">:</span><span class="w"> </span><span class="n">AsyncSocket</span><span class="p">)</span><span class="w"> </span><span class="sx">{.async.}</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="p">...</span>
<span class="w"> </span><span class="c"># These calls are equivalent</span>
<span class="w"> </span><span class="n">await</span><span class="w"> </span><span class="n">requestSocket</span><span class="p">.</span><span class="n">sendErrorResponse</span><span class="p">(</span>
<span class="w"> </span><span class="n">StatusCode</span><span class="p">.</span><span class="n">notFound</span><span class="p">,</span>
<span class="w"> </span><span class="s">"Not Found"</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="n">await</span><span class="w"> </span><span class="n">sendErrorResponse</span><span class="p">(</span>
<span class="w"> </span><span class="n">requestSocket</span><span class="p">,</span>
<span class="w"> </span><span class="n">StatusCode</span><span class="p">.</span><span class="n">notFound</span><span class="p">,</span>
<span class="w"> </span><span class="s">"Not Found"</span>
<span class="w"> </span><span class="p">)</span>
</code></pre></div>
<p>This means that any type can be “extended” in a sense just by writing procedures with that type as the first parameter, no need for sub-classing or a special extension method syntax.</p>
<h2>Blocks</h2>
<p>One neat little feature is that you can open a new code block anywhere, with or without a name, and as well as being visually separated from the code around it it will have its own scope. A <code>break</code> statement will break out of that block, but not the containing one.</p>
<p>I didn’t find much use for this in either of the projects I’ve worked on so far, but it’s definitely something I can see being useful for longer procedures and certain control-flow situations.</p>
<h2>Closures</h2>
<p>Nim supports passing around references to procedures, which allows for a number of neat constructs, including closures. The below procedure creates a closure that animates a spinner when called in a loop while waiting for an <span class="caps">IO</span> operation to conclude. It contains everything it needs, including a constant.</p>
<div class="highlight"><pre><span></span><code><span class="k">proc</span><span class="w"> </span><span class="nf">getDisplayProgressClosure</span><span class="p">():</span><span class="w"> </span><span class="n">proc</span><span class="p">()</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">phases</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">[</span><span class="s">"🮪"</span><span class="p">,</span><span class="w"> </span><span class="s">"🮫"</span><span class="p">,</span><span class="w"> </span><span class="s">"🮭"</span><span class="p">,</span><span class="w"> </span><span class="s">"🮬"</span><span class="o">]</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="n">lastTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">now</span><span class="p">()</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="n">initial</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kp">true</span>
<span class="w"> </span><span class="k">proc</span><span class="w"> </span><span class="nf">displayProgress</span><span class="p">()</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="k">let</span><span class="w"> </span><span class="n">elapsed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">now</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">lastTime</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">elapsed</span><span class="p">.</span><span class="n">inMilliseconds</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="n">initial</span><span class="p">:</span>
<span class="w"> </span><span class="n">lastTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">now</span><span class="p">()</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">initial</span><span class="p">:</span>
<span class="w"> </span><span class="n">erasePrevious</span>
<span class="w"> </span><span class="n">initial</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kp">false</span>
<span class="w"> </span><span class="n">styledEcho</span><span class="p">(</span>
<span class="w"> </span><span class="n">fgGreen</span><span class="p">,</span>
<span class="w"> </span><span class="o">&</span><span class="s">"{phases[phase]}"</span><span class="p">,</span>
<span class="w"> </span><span class="n">fgCyan</span><span class="p">,</span>
<span class="w"> </span><span class="s">" Retrieving holidays..."</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="n">inc</span><span class="p">(</span><span class="n">phase</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="n">phases</span><span class="p">.</span><span class="n">high</span><span class="p">:</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">displayProgress</span>
</code></pre></div>
<h2>Templates <span class="amp">&</span> Compile Time Execution</h2>
<p>One of the most exciting features of Nim, for me, is the ability to execute code at compile time, and otherwise manipulate the final state of the code.</p>
<p>For example to embed a file in a binary in C# you have to set a property against the file in the <span class="caps">IDE</span> (or maybe in the project file) to make it an embedded resource, and then do some reflection to pull it back out at runtime. In Nim, you can just call <code>readFile</code> and assign the result to a constant.</p>
<div class="highlight"><pre><span></span><code><span class="k">const</span><span class="w"> </span><span class="n">DEFAULT_BANDS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">readFile</span><span class="w"> </span><span class="s">"./config/bands.json"</span>
<span class="k">const</span><span class="w"> </span><span class="n">DEFAULT_CONFIG</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">readFile</span><span class="w"> </span><span class="s">"./config/luz.toml"</span>
</code></pre></div>
<p>There is also a compile-time branching statement, <code>when</code>. This is similar to the pre-processor <code>#if</code> in C#, or <code>#ifdef</code> in C, but it fits more naturally with the rest of the code.</p>
<p>Templates allow you to insert specified code in other parts of the codebase, with substitutions, before compilation. One use for this is as an alternative to short procedures, so the code gets inlined, saving a function call.</p>
<p>I feel like I’m only at the start of getting my head around this feature. I thought it might be a good way to output variations of a procedure for operating on different types, but I’m not sure the result is readable or concise enough to be worthwhile:</p>
<div class="highlight"><pre><span></span><code><span class="k">template</span><span class="w"> </span><span class="nf">createGetSetting</span><span class="p">(</span>
<span class="w"> </span><span class="n">valueType</span><span class="p">:</span><span class="w"> </span><span class="n">untyped</span><span class="p">,</span>
<span class="w"> </span><span class="n">argValueTypeGet</span><span class="p">:</span><span class="w"> </span><span class="n">untyped</span><span class="p">,</span>
<span class="w"> </span><span class="n">envValueTypeGet</span><span class="p">:</span><span class="w"> </span><span class="n">untyped</span><span class="p">,</span>
<span class="w"> </span><span class="n">confValueTypeGet</span><span class="p">:</span><span class="w"> </span><span class="n">untyped</span>
<span class="p">)</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="k">proc</span><span class="w"> </span><span class="nf">getSetting</span><span class="p">(</span>
<span class="w"> </span><span class="n">args</span><span class="p">:</span><span class="w"> </span><span class="n">Table</span><span class="o">[</span><span class="nb">string</span><span class="p">,</span><span class="w"> </span><span class="n">Value</span><span class="o">]</span><span class="p">,</span>
<span class="w"> </span><span class="n">arg</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span><span class="p">,</span>
<span class="w"> </span><span class="n">conf</span><span class="p">:</span><span class="w"> </span><span class="n">TomlValueRef</span><span class="p">,</span>
<span class="w"> </span><span class="n">confSection</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span><span class="p">,</span>
<span class="w"> </span><span class="n">confKey</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span><span class="p">,</span>
<span class="w"> </span><span class="n">env</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span><span class="p">,</span>
<span class="w"> </span><span class="n">default</span><span class="p">:</span><span class="w"> </span><span class="n">valueType</span>
<span class="w"> </span><span class="p">):</span><span class="w"> </span><span class="p">(</span><span class="n">valueType</span><span class="p">,</span><span class="w"> </span><span class="n">ConfigVariableSource</span><span class="p">)</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">default</span><span class="p">,</span><span class="w"> </span><span class="n">ConfigVariableSource</span><span class="p">.</span><span class="n">Default</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">arg</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">args</span><span class="p">:</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">args</span><span class="o">[</span><span class="n">arg</span><span class="o">]</span><span class="p">.</span><span class="n">kind</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="n">vkNone</span><span class="p">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="n">argValueTypeGet</span><span class="p">(</span><span class="n">args</span><span class="o">[</span><span class="n">arg</span><span class="o">]</span><span class="p">),</span>
<span class="w"> </span><span class="n">ConfigVariableSource</span><span class="p">.</span><span class="n">CommandLine</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="k">let</span><span class="w"> </span><span class="n">envStr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">getEnv</span><span class="p">(</span><span class="n">env</span><span class="p">,</span><span class="w"> </span><span class="s">""</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">envStr</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">""</span><span class="p">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="n">envValueTypeGet</span><span class="p">(</span><span class="n">envStr</span><span class="p">),</span>
<span class="w"> </span><span class="n">ConfigVariableSource</span><span class="p">.</span><span class="n">Environment</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="n">conf</span><span class="o">[</span><span class="n">confSection</span><span class="o">][</span><span class="n">confKey</span><span class="o">]</span><span class="p">.</span><span class="n">confValueTypeGet</span><span class="p">(),</span>
<span class="w"> </span><span class="n">ConfigVariableSource</span><span class="p">.</span><span class="n">ConfigFile</span>
<span class="w"> </span><span class="p">)</span>
<span class="k">proc</span><span class="w"> </span><span class="nf">splitOnComma</span><span class="p">(</span><span class="n">val</span><span class="p">:</span><span class="w"> </span><span class="nb">string</span><span class="p">):</span><span class="w"> </span><span class="nb">seq</span><span class="o">[</span><span class="nb">string</span><span class="o">]</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">val</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sc">','</span><span class="p">)</span>
<span class="k">proc</span><span class="w"> </span><span class="nf">getStringSequence</span><span class="p">(</span><span class="n">value</span><span class="p">:</span><span class="w"> </span><span class="n">TomlValueRef</span><span class="p">):</span><span class="w"> </span><span class="nb">seq</span><span class="o">[</span><span class="nb">string</span><span class="o">]</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="k">let</span><span class="w"> </span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">value</span><span class="p">.</span><span class="n">getElems</span><span class="p">()</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">@[]</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">values</span><span class="p">:</span>
<span class="w"> </span><span class="n">result</span><span class="p">.</span><span class="n">add</span><span class="w"> </span><span class="n">v</span><span class="p">.</span><span class="n">getStr</span><span class="p">()</span>
<span class="k">proc</span><span class="w"> </span><span class="nf">parseIntArg</span><span class="p">(</span><span class="n">val</span><span class="p">:</span><span class="w"> </span><span class="n">Value</span><span class="p">):</span><span class="w"> </span><span class="nb">int</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">parseInt</span><span class="p">(</span><span class="o">$</span><span class="n">val</span><span class="p">)</span>
<span class="n">createGetSetting</span><span class="p">(</span><span class="nb">string</span><span class="p">,</span><span class="w"> </span><span class="p">`</span><span class="o">$</span><span class="p">`,</span><span class="w"> </span><span class="p">`</span><span class="o">$</span><span class="p">`,</span><span class="w"> </span><span class="n">getStr</span><span class="p">)</span>
<span class="n">createGetSetting</span><span class="p">(</span><span class="nb">int</span><span class="p">,</span><span class="w"> </span><span class="n">parseIntArg</span><span class="p">,</span><span class="w"> </span><span class="n">parseInt</span><span class="p">,</span><span class="w"> </span><span class="n">getInt</span><span class="p">)</span>
<span class="n">createGetSetting</span><span class="p">(</span><span class="nb">bool</span><span class="p">,</span><span class="w"> </span><span class="n">toBool</span><span class="p">,</span><span class="w"> </span><span class="n">parseBool</span><span class="p">,</span><span class="w"> </span><span class="n">getBool</span><span class="p">)</span>
<span class="n">createGetSetting</span><span class="p">(</span><span class="nb">seq</span><span class="o">[</span><span class="nb">string</span><span class="o">]</span><span class="p">,</span><span class="w"> </span><span class="p">`</span><span class="o">@</span><span class="p">`,</span><span class="w"> </span><span class="n">splitOnComma</span><span class="p">,</span><span class="w"> </span><span class="n">getStringSequence</span><span class="p">)</span>
</code></pre></div>
<p>The result of the above code is four different procedures called <code>getSetting</code> which look for a setting in the command line arguments, an environment variable, or a config file, and return it as the expected type.</p>
<p>Even though the above code is a mess and I’m probably going to rethink it, I will say this - writing the template was surprisingly intuitive.</p>
<p>Nim’s meta-programming features become even more powerful with macros and pragmas, but I haven’t really gotten into them yet so I can’t say much about them.</p>
<h2>Standard Library</h2>
<p>There’s some pretty great stuff in the standard library, including very easy to use asynchronous http and networking libraries, and parsers for a variety of text-based file formats. Everything seems to be appropriately cross-platform as well. I haven’t got much else to say about it!</p>
<h2>Python Modules</h2>
<p>Something I’m always looking out for in a language is the ability to write Python modules in it. There seem to be a <a href="https://github.com/Pebaz/nimporter#nimporter" title="Nimporter on GitHub">couple</a> of Nim <a href="https://github.com/sstadick/nython#nython" title="Nython on GitHub">libraries</a> for <a href="https://medium.com/statch/speeding-up-python-code-with-nim-ec205a8a5d9c" title="Nimpy package benchmark">doing this</a>, both based on an underlying <a href="https://github.com/yglukhov/nimpy" title="Nimpy on GitHub">nimpy</a> library. They both look incredibly easy to use, but notably the support for exporting Python classes in nimpy seems to be experimental. It is also a bit unclear how it deals with Python objects as parameters of procedures rather than basic types.</p>
<p>My only point of comparison is <a href="https://cython.org/" title="Cython project homepage">Cython</a>, which is a really cool project that compiles Python code to C, and includes an optional extended syntax for optimisation, which is essentially writing C code but with a Python-like syntax. As cool as this is I think the breadth of options is confusing, and when you get down to writing optimised routines things start to break in very unhelpful C-like way - i.e. successful compiles and unceremonious runtime segfaults.</p>
<p>I much prefer the idea of writing modules in a language that is its own thing, and with Nim being as easy to write as it is, I’m excited to try it for this purpose.</p>
<h2>Conclusion</h2>
<p>I didn’t perform even rudimentary benchmarks, but I think it’s safe to assume that anything written in Nim will be faster than the equivalent Python code. Luz runs instantaneously, and Sparkle responds to requests almost instantaneously as well. Neither of them are doing anything that I wouldn’t expect Python to do at an acceptable speed under the same circumstances, however.</p>
<p>One thing about Nim benchmarks that I have seen is that they are generally performed with the <code>-d:danger</code> compiler flag, which disables all runtime checks. This is done in the name of “fairness” in comparison with C, but it doesn’t really seem fair to me if the norm for the language in production is <code>-d:release</code>.</p>
<p>I definitely found Nim very natural to develop in. Unlike Rust, which I also tried (<em>failed</em>) to learn recently, most of the concepts were already familiar to me from other languages, and the syntax was also very familiar. I often found myself writing correct Nim code first time, and where I made mistakes they were flagged during compilation in a way that was easy to understand. Runtime errors are also handled relatively gracefully - no segfaults even though Nim compiles to C, like Cython does.</p>
<p>Overall, a very interesting language that I look forward to doing more with.</p>
<p><a href="https://github.com/khoulihan/sparkle" title="Sparkle on GitHub"><img alt="Sparkle in action" src="https://blog.hyperlinkyourheart.com/images/nimpressions/sparkle_screenshot.png" title="It's called Sparkle because it's barely there..."></a></p>Recent Movie Watchings2022-05-09T14:00:00+02:002022-05-09T14:00:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2022-05-09:/various-movies.html<p>Movies I watched recently that I have a bit to say about… but not too much.</p><p>I’ve watched a lot of movies recently that I have a bit to say about, but not enough for a big post dissecting them on their own, like <a href="https://blog.hyperlinkyourheart.com/wrong-turn.html">Wrong Turn</a> and <a href="https://blog.hyperlinkyourheart.com/cybercentrism.html">Ready Player One</a>, so I’m just throwing them all together here.</p>
<h2>Kimi</h2>
<p><a href="https://www.imdb.com/title/tt14128670/?ref_=fn_al_tt_1">Kimi</a> is a 2022 psychological thriller about an agoraphobic woman, Angela, who works from home for a smart speaker company - creators of the eponymous “Kimi” - listening to supposedly anonymised audio clips that the speaker’s <span class="caps">AI</span> couldn’t understand. On one of the clips she hears what she believes to be an assault in the background, and when her employers are reluctant to investigate she has to (<em>gulp</em>)… leave her apartment!</p>
<p><a href="https://www.imdb.com/title/tt14128670/?ref_=fn_al_tt_1" title="First movie I've seen set during the pandemic!"><img alt="Screenshot from Kimi, of Angela out and about and wearing a mask" src="https://blog.hyperlinkyourheart.com/images/various-movies/kimi.jpg" title="First movie I've seen set during the pandemic!"></a></p>
<p>The main thing that I really liked about this movie was the portrayal of her struggle to leave her apartment, and the paradoxical sense of claustrophobia when she does. I felt much the same at one point in my life and it rang true to me.</p>
<p>On the other hand, when it gets down to <em>thriller time</em>, the action is quite repetitive and pointless. She gets captured, escapes, captured again almost straight away, escapes again right outside her building, and then there is somebody waiting for her in her apartment anyway. Boring. It gets better from there, but too late.</p>
<p>One thing I really didn’t like was the role of the smart speaker, Kimi. Although the plot early on does highlight a lack of privacy and data protection when Angela is able to find out whose speaker recorded the clips, and obtain further recordings, this is undermined by the plot being fundamentally about solving a murder thanks to the speaker’s ubiquitous surveillance. It then takes on a heroic role at the climax when Angela is able to outwit several hired goons by ordering it to do various things like cut the lights and play music and so on. Overall, I would say the movie comes down on the side of being pro corporate surveillance.</p>
<h2>Mary Shelley</h2>
<p><a href="https://www.imdb.com/title/tt3906082/?ref_=nv_sr_srsg_2">This 2017 historical drama</a> is about the life of Mary Shelley and the sources of inspiration for her novel Frankenstein. Turns out <em>men</em> are the real monster??</p>
<p><a href="https://www.imdb.com/title/tt3906082/?ref_=nv_sr_srsg_2" title="[Stares motherfuckerly]"><img alt="Screenshot from Mary Shelley, of Mary (played by Elle Fanning) in a bonnet" src="https://blog.hyperlinkyourheart.com/images/various-movies/Mary-Shelley.jpg" title="[Stares motherfuckerly]"></a></p>
<p>I enjoyed this one a lot. I read up about her a bit after watching it and it seems like it was a bit loose with some of the details of her life (like how many children she had, and when they died), but what am I a Mary Shelley scholar?</p>
<p>Like Frankenstein, it explores the theme of men’s irresponsibility towards the procreative act, and neglect of their progeny, but more explicitly, and as such it’s a great complement to the book. Interestingly, the male characters don’t really seem to get it, and focus on the idea that Frankenstein is about Mary alone feeling neglected, rather that a more general lack of responsibility on their part. She doesn’t correct them.</p>
<h2>The Death of Stalin</h2>
<p><a href="https://www.imdb.com/title/tt4686844/?ref_=fn_al_tt_1">The Death of Stalin</a> is a political black comedy from 2017 about the aftermath of Stalin’s death. I found it pretty funny, but it was also deeply weird to hear a bunch of undisguised American and British accents from characters in a movie set in the Soviet Union. Probably it would have been worse if they put on stereotypical Russian accents, of course, but Cockney Stalin?</p>
<p><a href="https://www.imdb.com/title/tt4686844/?ref_=fn_al_tt_1" title="Cockney Stalin?? Ridiculous!"><img alt="Screenshot from The Death of Stalin, of Stalin laughing right before he has a stroke" src="https://blog.hyperlinkyourheart.com/images/various-movies/Stalin.jpg" title="Cockney Stalin?? Ridiculous!"></a></p>
<p>As usual, I would probably prefer to see something from post-soviet creators examining their own history, through a satirical lens or otherwise.</p>
<h2>The Batman</h2>
<p><a href="https://www.imdb.com/title/tt1877830/?ref_=nv_sr_srsg_0">The Batman</a> is the latest in the saga of the Bat-men, this time starring Bobby Battinson. I think it might be my new favourite Batman movie, though I didn’t see the Ben Affleck one so I am not qualified to declare it the <em>objectively best</em> Batman movie.</p>
<p>The movie leans heavily into noir and gothic aesthetics, and imagines Bruce Wayne as a moody orphan who is uninterested in much outside of being a bat - including the effect his inherited wealth is having on society. Having become, under his father’s watch, a sort of slush fund for corruption, Bruce Wayne’s wealth is the underlying cause of much of the violence that Batman seeks to combat alongside his friends in the police.</p>
<p><a href="https://www.imdb.com/title/tt1877830/?ref_=nv_sr_srsg_0" title="Emomelon Wayne"><img alt="Screenshot from The Batman, of emo Bruce Wayne" src="https://blog.hyperlinkyourheart.com/images/various-movies/batman.jpeg" title="Emomelon Wayne"></a></p>
<p>His main adversary is the Riddler, portrayed here as a vigilante serial killer with shades of Seven’s John Doe and the Zodiac killer. While Batman is beating up common criminals and thugs, the Riddler targets the powerful and corrupt, and as such it’s hard to identify the villainy in his actions for much of the movie (aside from the fact that he’s, y’know, doing murders and all that). The general public certainly see him as a hero. Meanwhile, he sees himself and Batman as partners, playing off each other in a common crusade to clean up the city (and who else could, but the only two men smart enough to appreciate a good riddle). It isn’t until his plan to “wipe the scum off the streets” by flooding the city is revealed that we see his contempt for the innocent as well as the guilty.</p>
<p>Unfortunately the overall politics of the movie could probably be summed up as “we just need more good billionaires”. Bruce comes to realise that his vast wealth comes with responsibilities, and it seems like he’s going to do some philanthropy alongside his nightly costumed kickpunching. I guess we’ll find out in the sequel if enlightened liberal capitalism is the solution to capitalism’s problems.</p>
<p>I didn’t even realise that Colin Farrell was in this until I saw his name in the credits. He’s completely unrecognisable as the Penguin.</p>
<h2>Choose or Die</h2>
<p><a href="https://www.imdb.com/title/tt11514780/?ref_=fn_al_tt_1">Choose or Die</a> is a 2022 horror thriller about a cursed retro video game. This seemed like a fun premise, but unfortunately the movie as a whole was fucking crap.</p>
<p><a href="https://www.imdb.com/title/tt11514780/?ref_=fn_al_tt_1" title="There was a pixel art sequence leading up to this scene, out of nowhere"><img alt="Screenshot from Choose or Die, of Kayla and Isaac standing in front of Isaac's car, looking concerned" src="https://blog.hyperlinkyourheart.com/images/various-movies/cod.jpg" title="There was a pixel art sequence leading up to this scene, out of nowhere"></a></p>
<p>My main fault with it is that the game (named <span class="caps">CURS</span>>R) has apparently boundless powers to reshape reality to its whims, and that the choices it presents players with are seemingly arbitrary, and differ wildly in terms of their consequences. For example, the first choice the main character, Kayla, is given is between coffee and cake in a diner, with apparently no negative consequences. Another character’s first choice is between eating a computer or eating their own arm - both potentially fatal, one would think. For one of the “levels” of the game, Kayla is asked to choose between a blue door or a red one, with no other information. It reminded me of the first text-based video game I wrote when I was 7, which was just a collection of random scenarios where every path ultimately ended with the player being eaten by a tiger.</p>
<p>The climax sees Kayla facing off with a previous player (who we are introduced to in the opening scene, but learn very little about). At this point a moral is shoehorned in about white male entitlement in videogaming - which would be a fine theme if it wasn’t introduced so late and handled so clumsily.</p>
<p>I did like the grungy 80’s aesthetic, and that it seemed almost self-aware about how played out that kind of nostalgia is at this point. Also Asa Butterfield is great as a basement-dwelling retro video gaming obsessive. I do love me some Asa Butterfield…</p>
<p><img alt="Screenshot from Choose or Die, of Isaac (played by Asa Butterfield)" src="https://blog.hyperlinkyourheart.com/images/various-movies/asa.jpg" title="Buttery good"></p>Sim-Universe2022-04-16T15:43:00+02:002022-04-16T15:43:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2022-04-16:/simulation.html<p>Thoughts on the simulation argument.</p><p>I just done watched <a href="https://www.youtube.com/channel/UCrr7y8rEXb7_RiVniwvzk9w" title="Thought Slime">Thought Slime’s</a> <a href="https://www.youtube.com/watch?v=erkM0abWBfQ" title="Elon Musk is wrong about simulation theory, how uncharacteristic of him.">video about the simulation argument</a> (actually many months ago by the time I’m actually publising this), and it’s a topic about which I’ve had some thoughts myself, so I thought maybe it was time to write some of them down.</p>
<p>Like comrade Slime, I think that it’s an interesting thought experiment, but a lot of what is said about it is poorly thought through at best. It’s particularly frustrating when <a href="https://www.simulation-argument.com/simulation.html" title="Simulation Argument">Nick Bostrom’s argument</a> is held up as “proof” of the “certainty” that we are living in a simulation, alongside arguments and assertions that completely contradict it. The argument itself doesn’t claim to be proof of any such thing - it presents three possibilities based on premises about which we have almost no information.</p>
<h2>Why would we simulate?</h2>
<blockquote>
<p>One thing that later generations might do with their super-powerful computers is run detailed simulations of their forebears or of people like their forebears.</p>
</blockquote>
<p>This is Nick’s description of <em>what</em> futuristic super-computing civilisations would do with their computational power, but he doesn’t really get into <em>why</em> they might do this. Into this absence people pour all sorts of ideas. A common one is that we are equivalent to NPCs in a video-game. A related one is that we exist so that the simulators can pop in and out of our minds and ride us around for some reason - historical educational purposes perhaps, or the thrill of slumming it in the stupid-ages.</p>
<p>These are interesting concepts for science-fiction, but I don’t find them compelling as claims about the reality of our world. Video-games are indeed able to present more visually convincing realities than in the past, but they don’t do that by simulating entire physical universes in minute detail. They might run physics simulations for a variety of things in the vicinity of the player - beyond the bare minimum necessary to convince, they are hollow, simplified facades, and anything not relevant to the context of the current gameplay is non-existent. Similarly, what would it add to a player’s experience to have NPCs living lives outside of that context and having inner lives?</p>
<p>Nick Bostrum actually gets into some of the mechanisms that could be used to reduce the computational requirements of a simulation:</p>
<blockquote>
<p>If the environment is included in the simulation, this will require additional computing power – how much depends on the scope and granularity of the simulation. Simulating the entire universe down to the quantum level is obviously infeasible… But in order to get a realistic simulation of human experience, much less is needed – only whatever is required to ensure that the simulated humans, interacting in normal human ways with their simulated environment, don’t notice any irregularities.</p>
<p>Distant astronomical objects can have highly compressed representations: verisimilitude need extend to the narrow band of properties that we can observe from our planet or solar system spacecraft. On the surface of Earth, macroscopic objects in inhabited areas may need to be continuously simulated, but microscopic phenomena could likely be filled in ad hoc. What you see through an electron microscope needs to look unsuspicious, but you usually have no way of confirming its coherence with unobserved parts of the microscopic world</p>
</blockquote>
<p>The implicit assumption here is that the simulation is being made convincing for the benefit of the simulated minds (i.e. us), which always run at full resolution. Video-games are not run for the entertainment of NPCs however. If simulations are being run for the amusement of posthuman “players”, and they are interested in reducing the computational requirements, as Nick assumes, why would they not prune the most computationally expensive component - simulated human minds that are not immediately relevant to the player’s current experience? Would they even need to simulate fully conscious humans at all to provide convincing NPCs to players?</p>
<p>Nick does suggest something akin to such pruning in his original argument:</p>
<blockquote>
<p>In addition to ancestor-simulations, one may also consider the possibility of more selective simulations that include only a small group of humans or a single individual. The rest of humanity would then be zombies or “shadow-people” – humans simulated only at a level sufficient for the fully simulated people not to notice anything suspicious.</p>
</blockquote>
<p>However, it is again expressed as if the purpose of the simulation is solely to fool its unwitting inhabitant(s), with no proposed utility for the creators of the simulation.</p>
<p>I submit to you that if you are experiencing a private and mundane moment right now, and are conscious of it, you are probably not a character simulated on some posthuman equivalent of a PlayStation.</p>
<p>A more reasonable suggestion, to my mind, is that we would run such simulations in order to study our own civilisation at different stages of development, or to see how civilisations might develop under different circumstances. Would these simulations even require fully conscious simulated participants in order to be useful? Would they need to simulate the full lives of everybody who has ever lived? Or would they drastically reduce the number of minds needing to be simulated by cutting out all the boring parts? Would there really even be anything to be learned from such simulations?</p>
<p>This lack of clarity about why a posthuman civilisation would run ancestor simulations is at the heart of a lot of my issues with the argument. Without that understanding, we can’t really say whether such a civilisation would run them or not, or how many, or what their parameters would be. It’s just sort of assumed that they probably will because it would be a cool thing to be able to do, and some people say they would do it right now if it were possible. But that’s an easy thing to say when it’s impossible, and you don’t have to worry about the ethical concerns or the resources involved.</p>
<p>Another type of simulation we might run are of universes with different physical laws, but as the quotes above about simplifying the simulations suggest, these would have a different set of priorities, and wouldn’t really qualify as “ancestor simulations”. Whether they would even result in conscious entities would probably depend on the parameters of the simulation - they wouldn’t be the goal. If we take seriously the suggestion that we live in this kind of simulation, we can’t even assume that the simulators are anything like us, not even in their remote past, or that the simulating universe resembles ours in any way - so how can we possibly speculate about their motives, or what is computationally possible in their universe?</p>
<h2>Simulations Within Simulations</h2>
<p>One of the silliest suggestions that some people seem to take seriously is that the posthuman civilisation in the base reality would run simulations beyond the point where the simulated civilisations would be running their own simulations, with those simulations running further simulations, and so on.</p>
<p>Nick likens this scenario to running code in a virtual machine:</p>
<blockquote>
<p>It may be possible for simulated civilizations to become posthuman. They may then run their own ancestor-simulations on powerful computers they build in their simulated universe. Such computers would be “virtual machines”, a familiar concept in computer science. (Java script web-applets, for instance, run on a virtual machine – a simulated computer – inside your desktop.)</p>
</blockquote>
<p>His example is terrible, but the basic assertion is correct, a computer can simulate another computer in various ways, with varying levels of overhead. In the best case, code running in the virtual machine runs directly on the host hardware with no translation necessary. Obviously, this doesn’t add any processing power - software running in the host has to share its resources with the software running in the virtual machine.</p>
<p>Now, let’s think through this scenario a little bit.</p>
<p>Say you are a posthuman civilisation that has converted an entire planet into a giant computer. All the computation you decide to do is running on this computer. For some reason, you decide to run an ancestor simulation of your quite recent past, such that the simulated universe is on the cusp of achieving their own planet-computer. All of the computation of that universe would actually be running on <em>your</em> computer, alongside all the existing computation of your civilisation, and all the other work required for the simulation, all the fake stars and physics and advanced posthuman minds. Then you let them run their own simulation of their own recent past - now you have to support the load of three civilisations with planet-sized computers on only one actual physical planet-sized computer. And then four, and then five, and on and on.</p>
<p>A little while ago we were talking about cutting corners to save resources and focus on running our ancestors minds, and now here we are supporting an infinite regress of posthuman computers for no obvious purpose. There wouldn’t be any shortcuts here - if a computer 10 levels down wants to compute a hash or calculate millions of primes you would actually have to do the work or <em>they would know</em>.</p>
<p>There are two possible workarounds/objections to this that I can think of:</p>
<ol>
<li>Simulations could be run slower than the host reality to allow room for it. Would a time-dilated simulation be useful? I guess that depends on what you’re running it for!</li>
<li>Posthuman level simulations would only be allowed to develop once the host reality had converted enough matter to <em>pure computer</em> that supporting them was not a burden. In other words, the simulations would always have to lag behind by some significant amount.</li>
</ol>
<p>Fair enough, I guess that would do it, if keeping the simulations going is really important, you might always dedicate a proportional amount of your ever increasing computational resources to them. I do come back to the why though - would a simulation of a posthuman-level civilisation be a fun game for posthumans? Would there be anything to learn from it that you didn’t document when you were going through that phase?</p>
<blockquote>
<p>One consideration that counts against the multi-level hypothesis is that the computational cost for the basement-level simulators would be very great. Simulating even a single posthuman civilization might be prohibitively expensive. If so, then we should expect our simulation to be terminated when we are about to become posthuman</p>
</blockquote>
<p>Oh, well. Better to return to monke then, lest techno-god smite us for our arrogance.</p>
<h2>If God Did Not Exist…</h2>
<p>One possibility for why a posthuman civilization might choose not to run ancestor simulations is that doing so would raise some thorny ethical concerns. Take it away Nick:</p>
<blockquote>
<p>One can speculate that advanced civilizations all develop along a trajectory that leads to the recognition of an ethical prohibition against running ancestor-simulations because of the suffering that is inflicted on the inhabitants of the simulation</p>
</blockquote>
<p>Yes I think that might be likely… wait, what are you…</p>
<blockquote>
<p>However, from our present point of view, it is not clear that creating a human race is immoral</p>
</blockquote>
<p><em>Ooof</em>. It’s not just <em>creating a human race</em> that we’re talking about here, it’s <em>creating a human race and trapping them in a false reality for our own edification or amusement</em>, and in some hypothetical scenarios, <em>instantly terminating billions of them when they reach a certain level of development</em>. I think most people today would baulk at the prospect of treating even a single person like that, much less generation after generation of unwitting playthings.</p>
<p>Even worse are the moral implications for us, today, of taking some of Nick’s proposals seriously. In relation to the idea that many minds might be simulated only partially some amount of the time in order to save resources (discussed above), he suggests that it would also be a way for the simulators to avoid inflicting suffering:</p>
<blockquote>
<p>There is also the possibility of simulators abridging certain parts of the mental lives of simulated beings and giving them false memories of the sort of experiences that they would typically have had during the omitted interval. If so, one can consider the following (farfetched) solution to the problem of evil: that there is no suffering in the world and all memories of suffering are illusions. Of course, this hypothesis can be seriously entertained only at those times when you are not currently suffering.</p>
</blockquote>
<p>You weren’t traumatised, you see, you just have a false memory of trauma. And no need to worry about the consequences if you feel compelled to abuse, murder or rape: those are just zombie shadow-people you’re hurting, and they don’t really feel pain! Nothing is real and nothing matters!</p>
<p>But wait! Maybe our simulators will take it upon themselves to reward or punish us for our behaviour in their simulation (without informing us that they will do so, or on what basis), and dedicate ludicrous amounts of resources to simulating all the minds they have ever simulated, indefinitely, in an afterlife:</p>
<blockquote>
<p>Further rumination on these themes could climax in a naturalistic theogony that would study the structure of this hierarchy, and the constraints imposed on its inhabitants by the possibility that their actions on their own level may affect the treatment they receive from dwellers of deeper levels. For example, if nobody can be sure that they are at the basement-level, then everybody would have to consider the possibility that their actions will be rewarded or punished, based perhaps on moral criteria, by their simulators. An afterlife would be a real possibility.</p>
</blockquote>
<p>It genuinely disturbs me that there are people who are only good because they believe there is some force outside the universe that will reward them for it, or punish them for misbehaviour - and, even worse, people who would take on the role of cosmic arbiter themselves if given the chance.</p>
<h2>Postsingular Posthumans</h2>
<p>Inevitably, discussions about the simulation argument are little more than speculation based on almost no information. The kind of civilisation that would be capable of running such simulations would be one that has passed through a technological singularity - a point at which technological progress becomes so rapid that its path is impossible to predict. In fact the simulation argument requires that a civilisation has achieved the ability to simulate a human-equivalent mind - an Artificial General Intelligence - widely considered to be the invention that will instigate the singularity, since such an intelligence would probably be able to improve itself at an exponential rate.</p>
<p>We have zero examples of a post-singularity, posthuman civilisation, and only one example of a human-level civilisation, on which to base our speculations. What will super-intelligent posthumans value? Almost by definition such a civilisation would be beyond our comprehension.</p>
<p>The simulation argument seems mostly, to me, to be an attempt to imagine God in a way that is appealing to 21st century techies. I’m inclined to think that such a god, like all others, is not just unknowable, but non-existent.</p>Joplin & Syncthing2021-08-28T15:32:00+02:002021-08-28T15:32:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-08-28:/joplin.html<p>I recently switched from Evernote to Joplin, using Syncthing to sync my notes between devices.</p><p>I’ve been using Evernote for quite a few years now for keeping work and personal notes, despite never really being happy with it. Aside from the fact of entrusting my data to a private company, most of my issues with it were minor nuisances, and momentum kept me using it because I didn’t see a good alternative that seemed impressive enough to be worth the hassle of migration. I considered writing my own alternative many times, but of course there were even greater barriers to that!</p>
<p>A few weeks ago I decided to finally take the plunge and try out <a href="https://joplinapp.org/" title="Joplin's website">Joplin</a>, a <span class="caps">FOSS</span> note-taking desktop and mobile app.</p>
<h2>Joplin</h2>
<p>Migrating to Joplin was relatively easy, as it can import Evernote’s “<span class="caps">ENEX</span>” export format. Unfortunately Evernote made me <a href="https://help.evernote.com/hc/en-us/articles/209005557-Export-notes-and-notebooks-as-ENEX-or-HTML" title="How to export notes from Evernote">jump through a few hoops</a> to create these files - the web app, which I usually use, wouldn’t do it, and the export had to be done notebook by notebook.</p>
<p>Joplin did a fairly good job of converting the formatting to its native Markdown, but my notes were a mess anyway so it hardly mattered. The main thing that seemed to go wrong was headers being converted to bold text instead of actual header lines. I also had to reorganise the notebook hierarchy since the notebooks were exported and imported individually. With that done I was about where I had been with Evernote, albeit only on my main computer.</p>
<p>Joplin includes both a Markdown editor and a <span class="caps">WYSIWYG</span> editor. I haven’t tried the <span class="caps">WYSIWYG</span> editor because I like writing in Markdown these days, and I’m hoping using it will result in more structured notes than I’ve been keeping in the past. The default layout has the Markdown and rendered output side by side which I do find a little strange - I can never decide which side I should be reading. However, there is a button in the top right corner of the window to switch between dedicated editing, reading and side-by-side modes.</p>
<h2>Sync That Thing</h2>
<p>Joplin can sync between instances using all of the main cloud storage services as well as its own cloud offering. However, the method that appealed to me, because it keeps my notes out of anybody else’s hands, was to use <a href="https://syncthing.net/" title="Syncthing website">Syncthing</a>.</p>
<p>Syncthing is a <span class="caps">P2P</span> file synchronisation protocol and app that supports all the operating systems that I use (not iOS), and doesn’t require any complex network configuration.</p>
<p>Joplin is actually unaware of Syncthing - to use it, you need to select the “File system” sync target and point it to a folder. It will periodically export changes to this folder and import any changes it finds there.</p>
<p><img alt="Joplin sync settings" src="https://blog.hyperlinkyourheart.com/images/joplin/joplinsettings.png" title="Joplin sync settings"></p>
<p>Syncthing is managed via a web interface on localhost port 8384 by default. There are two main tasks to perform here - connecting your devices, and sharing your notes folder.</p>
<p>Devices in Syncthing are identified by quite unwieldy <span class="caps">SHA</span>-256 hashes, but it provides a number of ways to simplify exchanging these. Devices on the <span class="caps">LAN</span> are listed in the add device dialog, and if you’re using it on mobile there is an option to scan a <span class="caps">QR</span> code for the device you’re connecting. Devices have to grant permission to other devices that add them, and once they do you can choose which folders to share.</p>
<p><img alt="Add device" src="https://blog.hyperlinkyourheart.com/images/joplin/adddevice.png" title="Add device"></p>
<p>Adding a folder is just a matter of entering the path to it on the file system, and giving it a name. You can select existing remote devices to sharing with on the “Sharing” tab, or share it later. Devices have to approve shared folders as well, and you will have an opportunity to choose a target location at that point as well.</p>
<p><img alt="Add folder" src="https://blog.hyperlinkyourheart.com/images/joplin/addfolder.png" title="Add folder"></p>
<p>Syncthing on Android actually has two user interfaces, a native one and the same web <span class="caps">UI</span> as is available on desktop, which is a bit confusing. I found I had to drop down to the web <span class="caps">UI</span> to approve remote device and folder connections.</p>
<p>The Joplin configuration should be basically the same on any devices you want to sync - just choose the “File system” target and point to the synced notes folder. On desktop there is an option to clear the local notes and take everything fresh from the sync target, but the Android app seems to be missing this. As such, you might end up with multiple copies of Joplin’s initial documentation notes.</p>
<h2>Snags</h2>
<p>There are a few things to watch out for, and a few things that I personally find a bit confusing.</p>
<p>The first problem I encountered was due to my hesitation about where to have my phone’s photos stored on my laptop. I accepted the share to one location initially, and when I later deleted and recreated the share in another location I somehow orphaned 33 files. My phone is still stuck at 99% synced as a result.</p>
<p>One thing that appears strange to me is that you can share a folder from one device to another, and then share it from the second device to a third without the third device being aware of the first. I’m not sure if there are any consequences to that setup or if i is functionally the same as having all the devices aware of each other.</p>
<p>On the Joplin side, the formats of the sync repository occasionally need to be updated for new versions of the software. It then becomes unusable by older versions. It remains to be seen how much of an issue this will be - my main worry is that I will update one device beyond what is available on one of the others, or that I will be forced to update at an inconvenient time.</p>
<p>I have had one newly created note fail to sync to my phone so far, though it went to another device, and notes created subsequently synced to it no problem. This may have been the result of one of the issues described above, but I haven’t figured it out yet.</p>
<p>The final thing to be aware of is that Syncthing won’t try to resolve conflicts between files, instead choosing and renaming a “loser” when conflicts occur. I’m not sure what Joplin will make of the renamed files, but it’s something to be aware of if you’re moving between devices and possibly updating the same note before it can be synced.</p>
<h2>Beyond Notes</h2>
<p>Syncthing has actually been a revelation for me. As well as my notes I’ve been using it to sync photos from my phone to my laptop (previously I was relying on Google Photos), and for sending miscellaneous files from my laptop to my phone (previously Google Drive’s job). I’ve also been using it to send video files to my phone, something I wasn’t even bothering with before.</p>
<p>It feels great to be able to cut Google out of the loop as well as Evernote, and so far it has been working away well in the background without me having to think much about it after the initial setup.</p>Out of Road2021-08-11T17:10:00+02:002021-08-11T17:10:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-08-11:/out-of-road.html<p>Just a fancy car that took a wrong turn.</p><p><a href="https://portfolio.hyperlinkyourheart.com/out-of-road.html"><img alt="Out of Road" src="https://blog.hyperlinkyourheart.com/images/out-of-road/Denial02.png" title="Out of Road"></a></p>
<p>I don’t remember where I got the inspiration for this one. I guess I’ve been thinking a lot about climate change recently, what with the great crypto/<span class="caps">NFT</span> debates earlier in the year and recent extreme weather events and wildfires. It seems particularly timely given the recent <a href="https://arstechnica.com/science/2021/08/new-ipcc-climate-report-is-the-clearest-guidebook-for-selecting-a-future/" title="IPCC report"><span class="caps">IPCC</span> report</a>.</p>
<p>In terms of technique, I used a 3D render as a base, which is something I’ve done before, but this time I used somebody else’s <span class="caps">CC</span> licensed model because cars are kinda complex.</p>
<p><a href="https://skfb.ly/6W8x8"><span class="dquo">“</span>Ford Mustang Mach 1”</a> by BaldGuyMartin is licensed under <a href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution</a>.</p>
<h2>Timelapse</h2>
<p><a href="https://www.youtube.com/watch?v=21VjoNfGcDc"><img alt="Out of Road" src="https://img.youtube.com/vi/21VjoNfGcDc/0.jpg"></a></p>Wrong Turn… into Wokeness2021-07-05T19:40:00+02:002021-07-05T19:40:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-07-05:/wrong-turn.html<p>Conservatives fail to understand their own propaganda - but I guess it has the intended effect nonetheless.</p><blockquote>
<p>Beyond woke, yet the unintended result is the victims are punished for being woke. Skip it. This is not a Wrong Turn movie.</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>David J, May 02, 2021 - 1.5/5 stars on rottentomatoes.com</em></p>
</blockquote>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">Content warning: homophobia, misogyny, racism, spoilers for the movie Wrong Turn (2021)</span></p>
<p>It seemed pretty clear to me after watching <a href="https://www.imdb.com/title/tt9110170/?ref_=ttmi_tt" title="IMDB entry for Wrong Turn (2021)">Wrong Turn</a> that it had a conservative message. I haven’t seen the previous movies in the franchise, but I understand they are based on some unfair stereotypes about Appalachian people, so it seems a fair enough twist even if it doesn’t resonate with me personally. When I looked at the <a href="https://www.rottentomatoes.com/m/wrong_turn_2021/reviews?type=user&intcmp=rt-scorecard_audience-score-reviews" title="Audience reviews on Rotten Tomatoes">audience reviews on Rotten Tomatoes</a>, however, I discovered that a good many people who seemed like they would be on-board with that perspective instead understood it to be “woke” propaganda.</p>
<h2>The Message</h2>
<p>In Wrong Turn (2021), a young, white, American woman, Jen, throws off the shackles of a guaranteed prominent position in her father’s construction business to hike the Appalachian trail with her boyfriend and their friends. Despite warnings from the locals, they stray off the well-worn path, and fall victim to a primitivist cult known as The Foundation.</p>
<p>It’s clear from the start that Jen is the character that the audience are supposed to relate to. She’s torn between the path her father has laid out for her and the ideals of her friends, and is never shown to have taken those ideals to heart personally. She is more down-to-earth and capable than the other characters - she is the one that has to change the tyre when they get a flat en-route, for example, and later she is the only one able to think on her feet in high-pressure situations.</p>
<p><img alt="A woman changing a tyre is obviously peak woke" src="https://blog.hyperlinkyourheart.com/images/wrong-turn/tyre_change.jpg" title="A woman changing a tyre is obviously peak woke"></p>
<p>Her boyfriend, Darius, is an idealistic, politically active black man who expresses socialist and environmentalist ideas. Their friend group are well educated young urbanites, and include a gay couple - pretty much every review of the movie describes them as “diverse”. They’re incredibly cringey to be honest - when an aggressive local accuses them of never having done a day’s work in their lives they actually start bragging about their educational achievements and white-collar jobs - all except our hero, Jen, who is just a little lost in life.</p>
<p><img alt="Wokevengers assemble!" src="https://blog.hyperlinkyourheart.com/images/wrong-turn/wokevengers.jpg" title="Wokevengers assemble!"></p>
<p>Not long after setting out they divert from the trail to find a civil-war fort that one of them is interested in. They quickly become lost, and fall victim to various traps before finally encountering, and murdering, a member of the Foundation. Shortly thereafter they are captured and taken to the Foundation’s camp.</p>
<p>Finally we learn what the villains of the piece are all about - not inbred hill people as expected, but an egalitarian, primitivist, socialist cult whose leader has an impeccable hipster coiffure. The surviving friends are put on trial, and their every defense is twisted back on them - <em>they</em> are the intruders! <em>They</em> rushed to judgement based on appearances! <em>They</em> murdered someone in cold blood! <em>They</em> need to respect the Foundation’s culture! Such hypocrites!</p>
<p><img alt="King of the hipsters" src="https://blog.hyperlinkyourheart.com/images/wrong-turn/venable.webp" title="King of the hipsters"></p>
<p>Far from being inbred, this cult thrives on the recruitment of wayward travelers who they either brainwash into accepting their ideology, or blind with a hot poker and leave to fumble in a dark cave. Really subtle stuff.</p>
<p>Jen and Darius are the only survivors of the trial, and only because Jen convinces the group’s leader, Venable, that they could be useful members of the community. Jen, considering herself to have no relevant skills apparently, is only able to offer herself, as Venable’s wife.</p>
<p>As I mentioned earlier, Jen is actually the only member of the friend group that is portrayed as having any degree of competence or skill relevant to life in the real world. The rest of them are useless, out-of-touch, and varying degrees of obnoxious. But in the woke socialist utopia of the Foundation, she is only valued for her body.</p>
<p>As a result of her relationship with Venable, Jen ends up pregnant. Of course she never considers a termination.</p>
<p>It’s true that “woke” people are amongst the victims in this movie, but more importantly it is “wokeness” that is the monster, leading good All-American girls like Jen off the conservative middle-class path and into a life of bondage, exploitation and sin. She’s the character we’re supposed to find relatable - the rest of them deserve their fate because of their embrace of “woke ideology”, and their deaths are likely intended to be entertaining for that reason. They are so in tune with the villains that Darius chooses to stay with them instead of taking the opportunity to escape.</p>
<p>Let me have another go at summarising what I think this movie is trying to express.</p>
<p>In Wrong Turn (2021), a young, white, American woman is led astray by her “woke” boyfriend and her “woke” friends. While seeking to dig up civil war history that is best left buried, they encounter the logical end-point of “woke” ideology made manifest, and it abuses them horrifically. Jen escapes thanks to the savvy and skills instilled in her by her conservative upbringing, the refusal of her father to abandon his search for her, and the kindness of the misunderstood locals. She returns to her middle-class path through life by working in the family business, and violently rejects further attempts to lead her back to the horrors of “wokeness”.</p>
<p>In short, this is a conservative movie espousing conservative ideals. I disagree with David J, quoted above - the characters are intentionally punished for being “woke”.</p>
<h2><span class="dquo">“</span>Hollyweird is dying a slow death”</h2>
<p>Let’s take a look at some of the audience reviews on Rotten Tomatoes from people who seem to have missed the point.</p>
<blockquote>
<p>Its just more hand fisted political bs with pretty crappy character development.</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>Gage S, Apr 14, 2021 - 1.5/5 stars</em></p>
</blockquote>
<p>I guess you could interpret “hand fisted political bs” to be referring to the conservative political messaging. I choose not to.</p>
<blockquote>
<p><span class="dquo">“</span>Woke” America is destroying our culture Absolutely irredeemable</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>James A, Mar 14, 2021 - 0.5/5 stars</em></p>
</blockquote>
<p>It might seem like James gets it, if not for the 0.5 stars.</p>
<blockquote>
<p>I would have given this a four, if it wasn’t for the “over the top” libtardation seen in the movie that Hollyweird so much loves these days. The mixed race couple, the gay couple, the Arab guy, the Asian guy, Black guy wearing “Black Owned” T-Shirt, The racist Sheriff, White-Guilt guy gets mad, calling Confederate monument “Racism” like a White-Knight. Pretty embarrassing stuff. Original from 2003 was better, this wasn’t bad. Hollyweird is dying a slow death, stuff like this in movies is asinine…</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>Davis H, Mar 06, 2021 - 3/5 stars</em></p>
</blockquote>
<p>It’s like this guy stopped watching a third of the way through. Also a pretty explicit example of how the mere existence of characters who are not straight or white is unacceptable political content to some people.</p>
<blockquote>
<p>Another woke joke bad movie</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>michael b, Mar 07, 2021 - 0.5/5 stars</em></p>
</blockquote>
<p>Joke’s on you, buddy.</p>
<p>This next one’s pretty gross and misogynistic, and you won’t miss much if you choose to skip it.</p>
<blockquote>
<p>Revolves around a queer braindead friend group, which just so happens to have every single race in it. Due to the fact that the whore with the least amount of brain cells becomes a rambo bitch and survives till the end makes me not able to give this more than 4 stars. Giving this 4 stars because you get to see a dumbass friend group suffer. Also in the foundations court room they never mentioned how the foundation drew first blood with the gay dude getting a big log to the face for the last time.</p>
<p><i class="fas fa-comment-alt quote-icon"></i> <em>the b, Jun 10, 2021 - 2/5 stars</em></p>
</blockquote>
<p>I had to highlight this one because they understood at least part of what the movie was about - watching “woke” straw-people being punished. I can’t give this review more than 4 stars however because of their views on Jen - I guess because she has sex she must be a “dumb whore”. 2/5 stars.</p>
<h2>Final Observation</h2>
<p>What strikes me about these reviews is that they reveal how the movie reinforces a conservative (or more broadly right-wing) worldview regardless of whether the viewer actually understands the messaging. Either the messaging is understood and received as intended, or the mere presence of <span class="caps">POC</span> and gay characters reinforces a perception of a liberal Hollywood elite pushing a “woke” agenda.</p>Gemini Launch!2021-06-26T14:59:00+02:002021-06-26T14:59:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-06-26:/gemini-launch.html<p>All about the launch of my Gemini capsule, and how it is generated and hosted.</p><p>In the olden-times, before the Web became basically synonymous with the Internet itself in many people’s minds, there was another, competing hypertext protocol: <a href="https://en.wikipedia.org/wiki/Gopher_(protocol)" title="Gopher entry on Wikipedia">Gopher</a>.</p>
<p>I say “was”, but of course Gopher never really went away - it was kept alive by enthusiasts, and in recent years there has been a resurgence of interest in it as a sort of haven from the ubiquitous surveillance and relentless commercialisation of the Web.</p>
<p>I’ve long been interested in Gopher (I even <a href="https://hyperlinkyourheart.itch.io/gophers" title="Gophers on itch.io">made a game about it</a>), and have intended to start a phlog for a while without ever going ahead with it. Something about it always just seemed a little bit awkward and off-putting. I was torn between using Gophermap (i.e. menu) files for everything, or using plain text for posts and sacrificing any hypertextuality. I was torn between finding the need to wrap text to be cool and retro, or a hassle that results in an inferior experience for both creating and consuming content.</p>
<p><a href="https://gemini.circumlunar.space/docs/faq.gmi" title="Gemini FAQ">Gemini</a> is a new protocol which takes inspiration from both Gopher and the Web, and from a certain perspective, improves on both.</p>
<p>When I heard about Gemini I didn’t really get it at first. I thought it was just Gopher with <span class="caps">SSL</span>, which is nice, but I figured I’d get set up on Gopher first and then consider a Gemini mirror. A few days ago I saw a screenshot of the Lagrange browser on Mastodon and started to look into it a bit more. When I realised just how many issues of both Gopher and the web it addresses, I was hooked! I spent several days after that setting up <a href="gemini://gemini.hyperlinkyourheart.com/" title="My capsule">a capsule</a> (the Gemini equivalent of a “site”).</p>
<p><a href="gemini://gemini.hyperlinkyourheart.com/" title="My capsule"><img alt="My Gemlog in Lagrange" src="https://blog.hyperlinkyourheart.com/images/gemini-launch/gemlog.png" title="My Gemlog in Lagrange"></a></p>
<h2>Static Generation</h2>
<p>After experimenting with a Gemini server for a bit and creating a few static <code>text/gemini</code> files, I decided that I wanted to statically generate my gemlog the same way that I do my blog. I expected to have to write something from scratch to do this, but after some experimentation I was able to get the <a href="https://blog.getpelican.com/" title="Pelican static site generator">Pelican static site generator</a> (which I also use for my blog) to both read and output <code>.gmi</code> files. It does take a bit of configuration however, and I had to monkeypatch a couple of methods in Pelican.</p>
<p>Unfortunately this means that it is only guaranteed to work with the current version of Pelican, 4.6.0, and could break at any time. Nonetheless, the plugin is <a href="https://github.com/khoulihan/pelican-gemini" title="pelican-gemini plugin">available on GitHub</a> if you want to try it out.</p>
<h3>Gemini Reader</h3>
<p>The first thing required was a custom “Reader” that can handle <code>.gmi</code> files instead of the usual Markdown or reStructuredText files. It’s simple enough - it just parses the file up to the first blank line as metadata, and the rest of the content is returned unmodified, since we are also going to output the same format.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">GeminiReader</span><span class="p">(</span><span class="n">BaseReader</span><span class="p">):</span>
<span class="n">enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">file_extensions</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'gmi'</span><span class="p">,</span> <span class="s1">'gemini'</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">read</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<span class="n">metadata</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">content</span> <span class="o">=</span> <span class="s2">""</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s1">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="n">end_of_meta</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">while</span> <span class="ow">not</span> <span class="n">end_of_meta</span><span class="p">:</span>
<span class="n">current</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">readline</span><span class="p">()</span>
<span class="k">if</span> <span class="n">current</span> <span class="o">==</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">'</span> <span class="ow">or</span> <span class="n">current</span> <span class="o">==</span> <span class="s1">''</span><span class="p">:</span>
<span class="n">end_of_meta</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">continue</span>
<span class="n">current</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">split</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">': '</span><span class="p">)</span>
<span class="n">metadata</span><span class="p">[</span><span class="n">split</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">lower</span><span class="p">()]</span> <span class="o">=</span> <span class="n">split</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="c1"># After the first blank line, the rest is content.</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
<span class="n">parsed</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">metadata</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="n">parsed</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">process_metadata</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">content</span><span class="p">,</span> <span class="n">parsed</span>
</code></pre></div>
<h3>Handling Internal Links</h3>
<p>Pelican has a mechanism for linking to content internal to the site where you start the <span class="caps">URL</span> as <code>{static}</code> or <code>{filename}</code> and it replaces those with the appropriate paths during generation. However, this didn’t work with the Gemini link syntax - the replacement is based on a regular expression that assumes the placeholder will be found in an attribute of a <span class="caps">HTML</span> element.</p>
<p>I couldn’t find any setting or hook in the plugin system to alter this regular expression. There is a setting to customise the part that specifies the braces, so you could change the placeholders to <code>¿¿static??</code> or something if you like, as long as it is still found in <span class="caps">HTML</span>. It seemed like my only option was to replace the method where the problem regex pattern is defined, and use something that matches Gemini links instead.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">_get_intrasite_link_regex</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">intrasite_link_regex</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">settings</span><span class="p">[</span><span class="s1">'INTRASITE_LINK_REGEX'</span><span class="p">]</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s2">"(?P<markup>=> )(?P<quote>)(?P<path></span><span class="si">{}</span><span class="s2">(?P<value>[\S]*))"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">intrasite_link_regex</span><span class="p">)</span>
<span class="k">return</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">regex</span><span class="p">)</span>
</code></pre></div>
<p>You’ll notice this also has to include a “quote” group because that was present in the <span class="caps">HTML</span> version and was expected elsewhere - here it will always be an empty string.</p>
<p>Unfortunately, the problems didn’t end there. I found that the placeholders were removed, but not replaced with the absolute <span class="caps">URL</span> of the capsule. This turned out to be because urllib is used to join the <span class="caps">URL</span> components, and it doesn’t recognise the gemini protocol. To get around this I had to replace another method, and make a call to a wrapper around <code>urllib.urljoin</code>.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">_urljoin</span><span class="p">(</span><span class="n">base</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">is_gemini</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">'gemini://'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">is_gemini</span><span class="p">:</span>
<span class="n">base</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">'gemini://'</span><span class="p">,</span> <span class="s1">'https://'</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">urljoin</span><span class="p">(</span><span class="n">base</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">if</span> <span class="n">is_gemini</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">'https://'</span><span class="p">,</span> <span class="s1">'gemini://'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
</code></pre></div>
<h3>Gemini Output</h3>
<p>Pelican uses Jinja2 for its templating, which is happy to work with any type of text file, so creating <code>.gmi</code> templates wasn’t an issue. Handily, there is a setting to look for templates with extensions other than <code>.html</code>.</p>
<div class="highlight"><pre><span></span><code><span class="n">THEME</span> <span class="o">=</span> <span class="s1">'themes/hypergem'</span>
<span class="n">TEMPLATE_EXTENSIONS</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'.gmi'</span><span class="p">,</span> <span class="s1">'.gemini'</span><span class="p">]</span>
</code></pre></div>
<p>To get Pelican to output files with a <code>.gmi</code> extension instead of <code>.html</code>, there are a bunch of settings for the different parts of the site. A single “extension” setting like for the templates would be nice, but whatchagonnado? I took the opportunity to customise the article location and file names as well.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># These settings are required to output files as .gmi instead of .html</span>
<span class="n">ARTICLE_URL</span> <span class="o">=</span> <span class="s1">'articles/{date:%Y}-{date:%m}-{date:</span><span class="si">%d</span><span class="s1">}-</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">ARTICLE_SAVE_AS</span> <span class="o">=</span> <span class="n">ARTICLE_URL</span>
<span class="n">DRAFT_URL</span> <span class="o">=</span> <span class="s1">'drafts/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">DRAFT_SAVE_AS</span> <span class="o">=</span> <span class="n">DRAFT_URL</span>
<span class="n">PAGE_URL</span> <span class="o">=</span> <span class="s1">'pages/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">PAGE_SAVE_AS</span> <span class="o">=</span> <span class="n">PAGE_URL</span>
<span class="n">DRAFT_PAGE_URL</span> <span class="o">=</span> <span class="s1">'drafts/pages/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">DRAFT_PAGE_SAVE_AS</span> <span class="o">=</span> <span class="n">DRAFT_PAGE_URL</span>
<span class="n">AUTHOR_URL</span> <span class="o">=</span> <span class="s1">'author/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">AUTHOR_SAVE_AS</span> <span class="o">=</span> <span class="n">AUTHOR_URL</span>
<span class="n">CATEGORY_URL</span> <span class="o">=</span> <span class="s1">'category/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">CATEGORY_SAVE_AS</span> <span class="o">=</span> <span class="n">CATEGORY_URL</span>
<span class="n">TAG_URL</span> <span class="o">=</span> <span class="s1">'tag/</span><span class="si">{slug}</span><span class="s1">.gmi'</span>
<span class="n">TAG_SAVE_AS</span> <span class="o">=</span> <span class="n">TAG_URL</span>
<span class="n">ARCHIVES_SAVE_AS</span> <span class="o">=</span> <span class="s1">'archives.gmi'</span>
<span class="n">AUTHORS_SAVE_AS</span> <span class="o">=</span> <span class="s1">'authors.gmi'</span>
<span class="n">CATEGORIES_SAVE_AS</span> <span class="o">=</span> <span class="s1">'categories.gmi'</span>
<span class="n">TAGS_SAVE_AS</span> <span class="o">=</span> <span class="s1">'tags.gmi'</span>
</code></pre></div>
<h3>Theme</h3>
<p>I haven’t got much to say about this. I wanted the article links to be a bit more descriptive than just the date and title, so I did something similar to what medusae.space does and included the article summary, the category, and the tags.</p>
<p>It’s close to general purpose but not quite - I added a custom <code>SITELOGO</code> setting that is used on the index page with an <span class="caps">ASCII</span> art version of my logo generated using <a href="https://ascii-generator.site/" title="ASCII Generator">ascii-generator.site</a>, and there is also a custom template for the custom landing page. The index is renamed using a setting, and another page is renamed to index.gmi to take its place. This is so if I want to add content that isn’t generated by Pelican, I have the scope to do so.</p>
<div class="highlight"><pre><span></span><code><span class="n">INDEX_SAVE_AS</span> <span class="o">=</span> <span class="s1">'gemlog.gmi'</span>
</code></pre></div>
<div class="highlight"><pre><span></span><code><span class="n">Title</span><span class="o">:</span><span class="w"> </span><span class="n">Hyperlink</span><span class="w"> </span><span class="n">Your</span><span class="w"> </span><span class="n">Heart</span>
<span class="n">Date</span><span class="o">:</span><span class="w"> </span><span class="mi">2021</span><span class="o">-</span><span class="mi">06</span><span class="o">-</span><span class="mi">23</span><span class="w"> </span><span class="mi">22</span><span class="o">:</span><span class="mi">59</span>
<span class="n">Slug</span><span class="o">:</span><span class="w"> </span><span class="n">index</span>
<span class="n">Authors</span><span class="o">:</span><span class="w"> </span><span class="n">Kevin</span><span class="w"> </span><span class="n">Houlihan</span>
<span class="n">Summary</span><span class="o">:</span><span class="w"> </span><span class="n">Capsule</span><span class="w"> </span><span class="n">index</span>
<span class="n">URL</span><span class="o">:</span><span class="w"> </span><span class="n">index</span><span class="o">.</span><span class="na">gmi</span>
<span class="n">save_as</span><span class="o">:</span><span class="w"> </span><span class="n">index</span><span class="o">.</span><span class="na">gmi</span>
<span class="n">Template</span><span class="o">:</span><span class="w"> </span><span class="n">capsule_intro</span>
<span class="n">Status</span><span class="o">:</span><span class="w"> </span><span class="n">hidden</span>
</code></pre></div>
<h2>Hosting</h2>
<p>I’m serving the capsule using <a href="https://github.com/michael-lazar/jetforce" title="Jetforce github">Jetforce</a> from a first generation Raspberry Pi which I had lying around and haven’t done anything with in a while. There was nothing really involved in setting it up beyond what is described in the documentation, except that I installed it in a virtualenv.</p>
<p>I also took steps to make sure it is running as a dedicated user with no permissions to anything else on the system.</p>
<p><img alt="Real professional operation" src="https://blog.hyperlinkyourheart.com/images/gemini-launch/hosting.jpg" title="Real professional operation"></p>
<h2>Future</h2>
<p>I’m not sure what’s next, but I’m excited! I might discuss with the Pelican crew if there are any ways around the issues I encountered that I might have overlooked, or if it could be adapted to be more suited to non-<span class="caps">HTML</span> output. If not, maybe a Gemini fork is in order. I have no idea if there are further issues with it beyond the functionality that I’ve used.</p>
<p>I have quite a few posts to port over from my blog yet, and I need to get some image optimisation happening there like I have here. Besides that, I guess all I have to do is get to know the community!</p>
<h2>Visit My Capsule (and Beyond)</h2>
<p>If you’re already familiar with Gemini please <a href="gemini://gemini.hyperlinkyourheart.com/" title="My capsule">check out my capsule</a>.</p>
<p>If you’re not, well, I still encourage you to visit, but I should probably give you some guidance on getting started.</p>
<p>If you just want to dip your toes you can browse the Gemini network using a <span class="caps">HTTP</span> proxy (<a href="https://proxy.vulpes.one/gemini/gemini.hyperlinkyourheart.com" title="Vulpes.one proxy">here’s one</a>, and <a href="https://portal.mozz.us/gemini/gemini.hyperlinkyourheart.com" title="Mozz.us proxy">another</a>). For what I would consider the “full experience” you will need a dedicated browser. I’ve been using <a href="https://github.com/skyjake/lagrange" title="Lagrange browser GitHub">Lagrange</a>, and highly recommend it, but there are a <a href="https://gemini.circumlunar.space/software/" title="Gemini software list">whole bunch of others</a> if that doesn’t suit you. Many of them also support Gopher, which makes browsing both into a seamless experience outside of the modern Web.</p>
<p>When you want to move beyond my capsule, here are some others I recommend:</p>
<ul>
<li>Medusae.space, a content directory: <a href="gemini://medusae.space" title="Medusae.space content directory">gemini://medusae.space</a></li>
<li>Station, a social network with user accounts: <a href="gemini://station.martinrue.com/" title="Station - where capsuleers hang out">gemini://station.martinrue.com/</a></li>
<li>Geddit, an anonymous link aggregator: <a href="gemini://geddit.glv.one/" title="Geddit?">gemini://geddit.glv.one/</a></li>
<li>Astrobotany, a community garden game: <a href="gemini://astrobotany.mozz.us/" title="Astrobotany">gemini://astrobotany.mozz.us/</a></li>
<li>A gemlog post about the “smolnet”, which explains the appeal far better than I ever could: <a href="gemini://republic.circumlunar.space/users/maugre/200701-smolnet.gmi" title="The smolnet of smol things">gemini://republic.circumlunar.space/users/maugre/200701-smolnet.gmi</a></li>
</ul>
<p>I leave you with my anxious young poppy, Jennifer, on AstroBotany:</p>
<div class="highlight"><pre><span></span><code> O
|
\o
|o
\/
. , <span class="ge">_ . ., l, _</span> ., _ .
^ ' ` '
name : "Jennifer"
stage : anxious young poppy
age : 2 days
rate : 1st generation (x1.0)
score : 326788
water : |██████████| 100%
bonus : | | 2%
</code></pre></div>iRehabilitation2021-06-21T23:55:00+02:002021-06-21T23:55:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-06-21:/mac-rehab.html<p>Attempting to keep an old-ish MacBook useful and interesting with Xubuntu.</p><p>I’ve been plagued by temptation lately to buy a <a href="https://www.pine64.org/pinebook-pro/" title="Pinebook Pro">Pinebook Pro</a>. My current laptop is really a desktop replacement, a beast that can hardly last an hour untethered from a power socket. It’s usually not worth the hassle of extracting it from its tangled nest of cables when I want to compute elsewhere, and that’s fine - it’s the workhorse. But as a result, the idea of a light, efficient laptop is alluring, especially when it’s one that runs Linux.</p>
<h2>Out with the New</h2>
<p>However, I don’t really like to buy new devices without good reason. I already have another laptop that meets the criteria of being relatively light and portable - the MacBook Pro that served as my main work machine between 2015 and 2019 when my wife and I were floating around Ireland, France and Spain and living out of the back of our car. I have been using it as a more portable option already on occasion, but it has a few annoying problems:</p>
<ul>
<li>The battery isn’t in great shape, so while it’s a lot better than my main laptop, it’s nothing like that expected of the Pinebook Pro.</li>
<li>The <span class="caps">OS</span> is outdated. It’s demanding constantly that I update to a newer version of MacOS, but I don’t want to. Apparently it could run the latest version, but I don’t trust Apple to preserve the usability of old devices.</li>
<li>It runs MacOS. MacOS is fine - it’s a Unix, it’s not Windows… but it still has a lot of little annoyances, it’s proprietary, and to be honest, I’m bored with it.</li>
</ul>
<p>Basically it’s lost its shine, and isn’t fun to use anymore.</p>
<p><img alt="Happier times house-sitting in Belfast" src="https://blog.hyperlinkyourheart.com/images/mac-rehab/early-days.jpg" title="Happier times house-sitting in Belfast"></p>
<h2>A Cunning Plan</h2>
<p>One of Linux’s oft-heralded killer use-cases is in giving old hardware new life. I’ve never really used it for that explicit purpose - whenever I use Linux, it’s just because I prefer to use Linux, even if it happens to be on old or low-powered machines. This one isn’t exactly an ancient artifact, but I thought maybe installing a Linux distro with a lightweight desktop environment would help stretch the battery life and make it feel a bit snappier, more like a new machine.</p>
<p>The two distros I considered were <a href="https://elementary.io/" title="Elementary OS">ElementaryOS</a> and <a href="https://xubuntu.org/" title="Xubuntu">Xubuntu</a>. I’m not sure how lightweight Elementary’s <span class="caps">DE</span> is, but I liked the look of it, so I decided to try it out with an eye to maybe using it as my main <span class="caps">OS</span> some day.</p>
<p>First impressions were great - it only used <span class="caps">700MB</span> of <span class="caps">RAM</span> after booting (compared to nearly <span class="caps">4GB</span> for MacOS!), and the degree of visual flair and polish were incredibly impressive. Unfortunately a couple of things put me off - when I cut the <span class="caps">CPU</span> frequency to 800MHz I began to experience occasional lag, and at one point the shell crashed with no way to recover it!</p>
<p>I didn’t have any experiences like that with Xubuntu. I ran it for a whole day from a <span class="caps">USB</span> stick, installed a bunch of software, worked on a blog post, and had no issues - so that was that decision made! It’s definitely not as visually impressive as Elementary, but I’d rather a responsive system than a pretty one in this case.</p>
<h2>Installation</h2>
<p>Installation was pretty smooth, especially compared to installing Linux on PowerPC Macs back in the day. The only snag was with the proprietary wireless driver. This was easily enough installed using the “Additional Drivers” settings dialog when running from the <span class="caps">USB</span> stick, but after installation the required driver was no longer available. Having no other means to connect to a network, this was a serious problem!</p>
<p>Solving this involved a couple of steps. First I enabled the “<span class="caps">CDROM</span>” source in the Software <span class="amp">&</span> Updates settings, under the “Other Software” tab. This caused the driver to become available in the Additional Drivers dialog, but it wouldn’t install. The problem was that the live <span class="caps">USB</span> stick was mounted somewhere under <code>/media/kevin</code>, but apt expected it to be mounted at <code>/media/cdrom</code>, which didn’t even exist. Unmounting the <span class="caps">USB</span> stick and running the following commands sorted it out, and allowed me to connect to the WiFi to install and upgrade other packages.</p>
<div class="highlight"><pre><span></span><code>sudo mkdir /media/cdrom
sudo mount /dev/sdb1 /media/cdrom
sudo apt install bcmwl-kernel-source
</code></pre></div>
<h2>Results</h2>
<p>Unfortunately the results were not quite what I’d hoped. I performed a test where I played a movie and music on a loop under both OSes, and Xubuntu was down to 10% battery in 1 hour 46 minutes, while Mac <span class="caps">OS</span> took 2 hours and 28 minutes to reach the same level. This was with all cores throttled to 800MHz under Xubuntu, and MacOS doing whatever it does naturally to save energy, but full screen and keyboard backlight brightness on both.</p>
<p>While Xubuntu runs great, and is much more pleasant to use for me, it doesn’t seem to achieve the same battery life under similar loads. I’m torn now between the user experience I prefer under Xubuntu and the superior energy efficiency of MacOS… Or perhaps buying a Pinebook after all!</p>
<p><img alt="Cheers on a game-jam well done" src="https://blog.hyperlinkyourheart.com/images/mac-rehab/game-jam.jpg" title="Cheers on a game-jam well done"></p>Recent Art & Portfolio2021-06-08T00:20:00+02:002021-06-08T00:20:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-06-08:/recent-art.html<p>Belated art updates from the last 7 months.</p><p>Once again I have become neglectful of updating this blog with my artwork, so let’s do a roundup of the last uh… <em>7 months?!?!</em> and maybe I’ll try to get it back on track from here on. Though I do have a nice <a href="https://portfolio.hyperlinkyourheart.com" title="Portfolio">portfolio site</a> now that I have been keeping up to date, so if you really like my art you could be following that as well. More on that below.</p>
<h2>Socialist Revolutionaries Past <span class="amp">&</span> Future</h2>
<p>Last October I was trying to get back into the development of my game <a href="https://gamejolt.com/games/just-a-robot/185852" title="Just a Robot">Just a Robot</a>, and started as I always do by completely re-imagining its entire look. In this case I was inspired by the look of Soviet propaganda and Anarchist woodcuts.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/rise.html"><img alt="Robot Propaganda" src="https://blog.hyperlinkyourheart.com/images/recent-art/RobotPropaganda03_x1.png" title="Rise"></a></p>
<p>I followed that up in December with this tribute to the luxurious moustache of Irish revolutionary socialist James Connolly.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/james-connolly.html"><img alt="James Connolly" src="https://blog.hyperlinkyourheart.com/images/recent-art/JamesConnolly_x1.png" title="The Irish people will only be free, when they own everything from the plough to the stars"></a></p>
<h2>Spaaaace</h2>
<p>For the New Year I committed myself to working on more space and sci-fi themed stuff. I started with this depiction of a space station roughly based on the <span class="caps">ISS</span>.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/space-station.html"><img alt="Space station" src="https://blog.hyperlinkyourheart.com/images/recent-art/SpaceStation_x1.png" title="Space Station"></a></p>
<p>Also in January, a spaceship approaching Mercury, with some tricky perspective.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/mercury-approach.html"><img alt="Mercury approach" src="https://blog.hyperlinkyourheart.com/images/recent-art/MercuryApproach_169_x1.png" title="Getting warm"></a></p>
<p>In February I did another piece loosely inspired by Soviet propaganda, and a theme that seemed to be somewhat controversial - the idea of billionaires fleeing into space and leaving the rest of us to our fates. Some people took that as a literal prediction of future events, but I think of it in more allegorical terms - capital is ruining the natural and social environment without any sense of responsibility to the rest of us, while its masters can escape the consequences of their actions without <em>literally leaving the planet</em> - depicting them doing so is just a good way of describing the situation, in my opinion.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/flight.html"><img alt="Flight of the Billionaires" src="https://blog.hyperlinkyourheart.com/images/recent-art/Flight02_x1.png" title="Pretty rockets though..."></a></p>
<p>In March I brought it back to Earth and explored the same theme from another perspective, more or less.</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/left-behind.html"><img alt="Left Behind" src="https://blog.hyperlinkyourheart.com/images/recent-art/LeftBehind_x1.png" title="So long..."></a></p>
<p>I started a piece in April for Cosmonautics day, but I didn’t get it finished until June - the capsule and final stage of Vostok 1 in orbit. I did a <a href="https://blog.hyperlinkyourheart.com/out-of-gas-post-mortem.html" title="Out of Gas">game jam</a> and an <a href="https://twitter.com/http_your_heart/status/1393938661251690500" title="Diamond hill">oil painting</a> in the meantime though!</p>
<p><a href="https://portfolio.hyperlinkyourheart.com/vostok1.html"><img alt="Vostok 1" src="https://blog.hyperlinkyourheart.com/images/recent-art/Vostok1_Final_x1.png" title="Vostok 1"></a></p>
<p>And that pretty much brings us up to date!</p>
<h2>Portfolio</h2>
<p><a href="https://portfolio.hyperlinkyourheart.com" title="Portfolio">My portfolio site</a>, which went live in July 2020, is another statically-generated site based on <a href="https://blog.getpelican.com/" title="Pelican">Pelican</a>, but focused on image galleries instead of blog entries (using the standard gallery plugin). I wanted a central place to put my art that wasn’t a social media platform, and that would display it optimally. I’ve linked to it a few times from here despite never actually mentioning it.</p>
<p>It is inspired by the ideas of Matej Jan on <a href="https://medium.com/retronator-magazine/art-and-the-internet-a3281ba60a88" title="Art and the Internet">displaying art on the internet</a>, and attempts to display my pieces at the best integer scaling to fit in the browser window, on a background with appropriate contrast, and with no distractions.</p>
<p>Of course, I also wanted to keep it as small and responsive as possible, so it does this with about 3kB of javascript, 11kB of <span class="caps">CSS</span>, and a selection of minimal <span class="caps">SVG</span> backgrounds. Because all the art is pixel art, transferring it at 1x resolution and resizing it in the browser (as discussed in <a href="https://blog.hyperlinkyourheart.com/energy-usage-update.html" title="Energy Usage Update">recent</a> <a href="https://blog.hyperlinkyourheart.com/image-optimisation.html" title="Image Optimisation">posts</a>) keeps things extremely compact, with the entire portfolio currently only amounting to 390kB. The portfolio does a lot better job of displaying the art than this blog does though, here the images are just resized to max width without attempting an integer scaling.</p>
<p>I think it looks real nice and that my art looks real nice on it, so <a href="https://portfolio.hyperlinkyourheart.com" title="Portfolio">go check it out!</a></p>Image Optimisation2021-05-30T00:27:00+02:002021-06-01T13:20:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-05-30:/image-optimisation.html<p>Image optimisation experiments and a helpful Pelican plugin</p><p>In the <a href="https://blog.hyperlinkyourheart.com/energy-usage-update.html">last instalment</a> of my epic blogging saga I recounted my discovery that the index page of this site had grown to over 1.<span class="caps">7MB</span> of content when loaded fresh, largely due to the images. One of my goals for this site was that it be lean - fast to load and energy efficient - and it was not meeting that goal at all. Clearly avoiding Javascript and <span class="caps">CSS</span> frameworks was not enough!</p>
<p>I immediately started thinking about how to improve the situation. Though I had previously ruled out the approach used by <a href="https://solar.lowtechmagazine.com/about.html" title="Low-tech magazine solar powered web site">Low-tech magazine’s solar-powered website</a> because I didn’t think it would fit my aesthetic, I decided to see if dithering coloured, rather than monochrome, PNGs would work better for me, and improve on the size and quality of an appropriately-sized 75% quality <span class="caps">JPEG</span>.</p>
<h2><span class="caps">PNG</span> Thunderdome</h2>
<p>The one to beat - baseline 75% <span class="caps">JPEG</span>, Size: 16.1kb</p>
<p><img alt="melanie-pointing_alpha_baseline.jpg" class="poi-no-optimise" src="https://blog.hyperlinkyourheart.com/images/image-optimisation/melanie-pointing_alpha_baseline.jpg" title="melanie-pointing_alpha_baseline.jpg: 16.1kb"></p>
<p>Using <a href="https://python-pillow.org/" title="Pillow">Pillow</a> and the same <a href="https://github.com/hbldh/hitherdither" title="hitherdither library">hitherdither</a> library that Low-tech magazine used for their site, I iterated on a script that output hundreds of compressed variations of a given image using different dithering algorithms and parameters.</p>
<p>The best results I found were with the Bayesian algorithm with a 32 colour palette, a 2x2 matrix, and an image size half the expected display size. This produced a result that was relatively readable, and reminiscent of pixel art. The size savings varied by image - sometimes up to 10kB, but often only 2-3kB as for this example. These savings are modest compared to the loss in detail, and I think this approach could only be considered because of the unique aesthetic it produces.</p>
<p>Palette: 32, Dither: bayer, Threshold: 256/8-256-256/8, Order: 2, Size: 13.9kb</p>
<p><img alt="melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh8-1-8.png" class="poi-no-optimise" src="https://blog.hyperlinkyourheart.com/images/image-optimisation/melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh8-1-8.png" title="melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh8-1-8.png: 13.9kb"></p>
<p>The three “threshold” parameters expected by hitherdither were a bit of a mystery to me. Through trial and error I found that some produced much smaller images, but unfortunately not at a level of quality that I found acceptable. Lower palette sizes also resulted in savings, but below 32 colours they started to look too abstract and unreadable to me.</p>
<p>Palette: 32, Dither: bayer, Threshold: 256/2-256-256/2, Order: 2, Size: 9.6kb</p>
<p><img alt="melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh2-1-2.png" class="poi-no-optimise" src="https://blog.hyperlinkyourheart.com/images/image-optimisation/melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh2-1-2.png" title="melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh2-1-2.png: 9.6kb"></p>
<h2>A Challenger Appears</h2>
<p>I was about ready to commit to this approach and start converting all the images when my wife reminded me that <span class="caps">WEBP</span> exists! After converting a few of my test images to <span class="caps">WEBP</span> it was clear that it had my dithered PNGs beat - half the size of the <span class="caps">JPEG</span> without any loss of quality.</p>
<p>80% quality <span class="caps">WEBP</span>, Size: 9.9kb</p>
<p><img alt="melanie-pointing_alpha_baseline.webp" class="poi-no-optimise" src="https://blog.hyperlinkyourheart.com/images/image-optimisation/melanie-pointing_alpha_baseline.webp" title="melanie-pointing_alpha_baseline.webp: 9.9kb"></p>
<p>Apparently <a href="https://caniuse.com/?search=webp" title="WEBP support">support for <span class="caps">WEBP</span></a> is pretty good these days, but there are a couple of annoying outliers - Safari only supports it on Big Sur, and <span class="caps">IE11</span> still exists, as I’m sure it always will.</p>
<p>As such, I decided I should probably try and fallback gracefully to a <span class="caps">JPEG</span> or <span class="caps">PNG</span> where <span class="caps">WEBP</span> isn’t supported. This can be achieved using <code><picture></code> and <code><source></code> elements to allow the browser to choose the format it likes best.</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">picture</span><span class="p">></span>
<span class="p"><</span><span class="nt">source</span> <span class="na">type</span><span class="o">=</span><span class="s">"image/webp"</span> <span class="na">srcset</span><span class="o">=</span><span class="s">"{optimal image url}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">source</span> <span class="na">type</span><span class="o">=</span><span class="s">"image/jpeg"</span> <span class="na">srcset</span><span class="o">=</span><span class="s">"{compatible image url}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"{compatible image url}"</span><span class="p">/></span>
<span class="p"></</span><span class="nt">picture</span><span class="p">></span>
</code></pre></div>
<h2>Let’s Automate</h2>
<p>The above <span class="caps">HTML</span> snippet presents a problem - my posts are not written in <span class="caps">HTML</span> but in Markdown, and processed by <a href="https://blog.getpelican.com/" title="Pelican static site generator">Pelican</a> into <span class="caps">HTML</span>, and that process just results in an <code><img></code> tag by default.</p>
<p>I threw together <a href="https://github.com/khoulihan/pelican-optimise-images" title="pelican-optimise-images plugin">a quick Pelican plugin</a> to post-process the generated <span class="caps">HTML</span> and replace any <code><img></code> tags with <code><picture></code> tags, if the referenced images could be replaced with WEBPs. It also processes the referenced images to create scaled <span class="caps">JPEG</span>/<span class="caps">PNG</span> versions as well as the <span class="caps">WEBP</span> version, so I don’t have to do any of that manually either.</p>
<h2>Results</h2>
<p>The index of the blog is now 778kB at the time of writing - so reduced by over half! I did also replace a particularly large and troublesome <span class="caps">GIF</span> with a static image as well, and converted some PNGs to JPEGs. This results in greater savings because the plugin converts PNGs to lossless WEBPs and JPEGs to lossy ones.</p>
<p>The plugin is actually not even working fully yet - for some reason it is missing some <code><img></code> tags on the index pages, leaving them serving their original unprocessed files.</p>
<p>I also haven’t done anything about the pixel art images, which if served at their original resolution could be a significant saving.</p>
<p>So in short, <a href="https://www.websitecarbon.com/website/blog-hyperlinkyourheart-com/" title="76th percentile for energy efficiency">huge progress</a>, but still much scope for improvement!</p>
<p>I am almost sorry I’m not going to end up using these dithered PNGs though…</p>
<p><img alt="Tracer and Chun-Li" class="poi-no-optimise" src="https://blog.hyperlinkyourheart.com/images/image-optimisation/tracer_alpha_halved_pal32_dithbayer_order2_thresh8-1-8.png" title="Wow looks like videogams"></p>
<h3>Update 01/06/2021</h3>
<p>I fixed the problem with the plugin and all images are now optimised except the few in this post that are flagged to be skipped. I also swapped out all the pixel art images and gifs for 1x resolution versions. These are just being scaled up by <span class="caps">CSS</span>, with reasonable results.</p>
<p>The index page is now 539kB, the whole site is just over <span class="caps">1MB</span>, and it is in the 80th percentile for energy usage according to websitecarbon.com (whatever that’s worth). I think the link above showed 75th or 76th percentile or thereabouts at the time of posting, but it will show 80th now.</p>Energy Usage Update2021-05-27T13:20:00+02:002021-05-27T13:20:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-05-27:/energy-usage-update.html<p>An update on the energy efficiency of this blog.</p><p>When I <a href="https://blog.hyperlinkyourheart.com/remember-blogs.html">started this blog</a>, one of my <a href="https://blog.hyperlinkyourheart.com/embedding-svgs.html">goals</a> was for it to be lean and efficient, both to improve the reader’s experience and to reduce the power usage involved in serving it.</p>
<p>I’ve been planning recently to write a post on a related topic - the energy efficiency of the Python language and how using it as the backend or <span class="caps">API</span> of a more dynamic website would fit with those same goals. In preparation for that, I thought to check in on this blog and whether it is still as lean as it was back when I was figuring out how to embed the <span class="caps">SVG</span> icons in it.</p>
<p>Unfortunately, it seems that it is not. Unlike one of my inspirations, <a href="https://solar.lowtechmagazine.com/2018/09/how-to-build-a-lowtech-website.html">low-tech magazine’s solar powered website</a>, I never found a solution for reducing the size of image files. For my posts about <span class="caps">TV</span> shows and movies I use fairly low quality JPEGs. But for my art and game dev posts I have been unwilling to compromise, and, frankly, a bit lazy, and have just been throwing large PNGs and GIFs into my posts without thinking about their size.</p>
<p>As a result, loading the index of this site now transfers 1.<span class="caps">7MB</span> if nothing is already cached. The text content and <span class="caps">CSS</span> are in the tens of kilobytes, with the rest being images. This is comparable to the initial load of many corporate news sites and large blogs with all their tracking javascript and ads blocked - an improvement over the norm perhaps, since I’m not doing any of the tracking or advertising that have become such a burden on the web, but not particularly notable. This site is now only in the <a href="https://www.websitecarbon.com/website/blog-hyperlinkyourheart-com/">57th percentile according to websitecarbon.com</a>.</p>
<p>For pixel art, one solution does present itself immediately. For my <a href="https://portfolio.hyperlinkyourheart.com">portfolio site</a>, all of the artwork is sent to the browser at 1x resolution and resized there by an integer factor by a small amount of javascript, and perhaps the same thing could be achieved with css. The index of that site is under 400kB despite including all of my artwork, and does not require specially resized image thumbnails or anything like that. I could do the same thing here instead of throwing manually resized art into posts.</p>
<p>I’m less sure what to do about screenshots and screencaps. The solution used by low-tech magazine doesn’t really work for me, I think it compromises the readability of the images too much and doesn’t fit this site’s aesthetic. I tried a number of different techniques for vectorising, posterising and dithering screencaps from <span class="caps">TV</span> shows and movies to reduce the number of colours, but they didn’t result in significant savings. On the other hand, they did actually look kind of cool and stylised!</p>
<p><img alt="Vectorised Beth Harmon" src="https://blog.hyperlinkyourheart.com/images/energy-usage-update/parkchesscrowd_vector.jpg" title="Vectorised Beth Harmon"></p>
<p>Something I should probably try is scaling such images down significantly and then back up to the required display size in <span class="caps">CSS</span>. Obviously this will degrade the quality, but it might be an acceptable trade-off.</p>
<p>Screenshots of things like a text-editor or <span class="caps">IDE</span> are even more problematic - if the quality is degraded there they are probably not useful. I will probably just have to try to keep that kind of thing to a minimum, and use tighter crops where necessary.</p>
<p>It’s easy to become complacent about efficiency when computers are so powerful that a few tens or hundreds of kilobytes don’t seem to cause them any strain, but when you have that mindset they can quickly add up to quite a significant waste of time and power. I think this is well demonstrated by <a href="https://daringfireball.net/linked/2018/05/27/usa-today-gdpr"><span class="caps">USA</span> Today’s pissy response to the <span class="caps">GDPR</span></a>, where instead of the usual 5.<span class="caps">2MB</span> bloated garbage site, readers from the <span class="caps">EU</span> were served a lean, content focused site that loaded almost instantly and didn’t track them.</p>
<p>Nothing I do is going to have the kind of impact that a site like <span class="caps">USA</span> Today improving their efficiency would have, purely because of the scale of the traffic involved, but I still want to try to do better.</p>Ludum Dare 48 Results2021-05-27T11:11:00+02:002021-05-27T11:11:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-05-27:/out-of-gas-results.html<p>Results for Ludum Dare 48 are out, and I did less well than last time!</p><p>Ludum Dare 48 results are out a while now, but I forgot to post about them. I did not do as well as last time, but since my <a href="https://blog.hyperlinkyourheart.com/gophers-results.html">results for Gophers</a> were exceptional for me, that was probably to be expected. This time I placed 433rd overall, with the stand-out category being graphics, where I just about broke the top 100, at 99th place.</p>
<table>
<thead>
<tr>
<th style="text-align: left;">Category</th>
<th style="text-align: right;">Rating</th>
<th style="text-align: right;">Placing</th>
<th style="text-align: right;">Percentile</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Overall</td>
<td style="text-align: right;">3.81</td>
<td style="text-align: right;">433</td>
<td style="text-align: right;">84th</td>
</tr>
<tr>
<td style="text-align: left;">Fun</td>
<td style="text-align: right;">3.259</td>
<td style="text-align: right;">1044</td>
<td style="text-align: right;">61st</td>
</tr>
<tr>
<td style="text-align: left;">Theme</td>
<td style="text-align: right;">3.672</td>
<td style="text-align: right;">873</td>
<td style="text-align: right;">67th</td>
</tr>
<tr>
<td style="text-align: left;">Innovation</td>
<td style="text-align: right;">3.345</td>
<td style="text-align: right;">754</td>
<td style="text-align: right;">72nd</td>
</tr>
<tr>
<td style="text-align: left;">Humor</td>
<td style="text-align: right;">3.596</td>
<td style="text-align: right;">277</td>
<td style="text-align: right;">89th</td>
</tr>
<tr>
<td style="text-align: left;">Graphics</td>
<td style="text-align: right;">4.466</td>
<td style="text-align: right;">99</td>
<td style="text-align: right;">96th</td>
</tr>
<tr>
<td style="text-align: left;">Audio</td>
<td style="text-align: right;">3.621</td>
<td style="text-align: right;">526</td>
<td style="text-align: right;">80th</td>
</tr>
<tr>
<td style="text-align: left;">Mood</td>
<td style="text-align: right;">3.846</td>
<td style="text-align: right;">460</td>
<td style="text-align: right;">83rd</td>
</tr>
</tbody>
</table>
<h2>Graphs</h2>
<p>A couple of graphs demonstrating my <em>trends</em>.</p>
<p><img alt="Ratings Graph" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-results/overall_graphics_ratings.png">
<img alt="Placings Graph" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-results/placings.png"></p>
<p>Still haven’t surpassed that Rattendorf peak, <em>sigh</em></p>
<h2>More Out of Gas</h2>
<p>I am committed to working on a post-jam release of this game, as I think with a few tweaks and a bit more content it could be something quite special (more special than the ratings above suggest anyway!)</p>
<p>I will be posting here with development updates, and also over on the <a href="https://hyperlinkyourheart.itch.io/out-of-gas">itch.io</a> page, where you can also still play the jam version for now.</p>Out of Gas2021-05-04T10:41:00+02:002021-05-29T22:52:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-05-04:/out-of-gas-post-mortem.html<p>A post-mortem of my Ludum Dare 48 entry, Out of Gas - a space-road-trip themed narrative/clicker game about a couple of outlaws fleeing their cryptocurrency debts.</p><p><a href="https://hyperlinkyourheart.itch.io/out-of-gas"><img alt="Out of Gas" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/OOG_itch_banner.jpg"></a></p>
<p><a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas">Out of Gas</a> is my entry for the recent <a href="https://ldjam.com/events/ludum-dare/48" title="Ludum Dare 48">Ludum Dare 48</a> game jam. It was intended to be a blatant <a href="https://subsetgames.com/ftl.html" title="FTL game"><span class="caps">FTL</span></a> rip-off, but in having much simplified clicker-like combat mechanics and an unusual cars-in-space aesthetic, I think it is sufficiently its own thing (I hope anyway!)</p>
<h2>Concept</h2>
<p>My initial thought was more of a straight sci-fi concept of travelling into deep space, facing combat and resource management challenges similar to those which ended up in the game. The first thing that came to mind as a title for this was “Out of Gas”, after the <a href="https://www.youtube.com/watch?v=-_heR2ekoxI" title="Out of gas. Out of road. Out of car I don't know how I'm gonna go.">Modest Mouse song</a> of the same name.</p>
<p>Thinking about that song got me thinking in another direction - essentially the same mechanics, but Earth-bound, set in a Modest Mouse world of highways and drifters, fleeing problems and starting over. The problem to flee in this case would obviously be debt - something that you can get deeper and deeper into.</p>
<p>As you can probably tell by now I ended up combining these two ideas! I didn’t want to let either of them go, and I quickly fell in love with the idea of a universe where space combat meant winding down your window and firing a handgun at your enemy.</p>
<p>The combined concept suggested that a new, sci-fi twist on the debt problem was required, and that’s where the idea of a constantly growing cryptocurrency debt came in. I’ve been thinking about cryptocurrencies a lot lately since they’re going through another hype cycle. One commonly touted feature of them (or some, like Bitcoin, at least), is that they’re deflationary, so any debt denominated in them would constantly grow in value instead of decreasing in value like debts denominated in inflationary fiat currencies. This seemed like the perfect notion for the bizarre world of my game. It doesn’t really have any impact on the gameplay, but I think it’s a nice bit of narrative flavour, and it fits the theme perfectly.</p>
<p><img alt="Late payment" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/OOG-Late-Payment.jpg"></p>
<p>The final piece of the conceptual puzzle was the characters. I couldn’t very well have two anonymous nobodies riding along with the player on a trip like this! I turned to two characters I had vague notions of making a game about years ago, Haze <span class="amp">&</span> Lee, a gun-toting, post-apocalyptic outlaw couple. Unfortunately I don’t think their personalities really come across in the game due to time constraints, but at least they have names!</p>
<h2>Art</h2>
<p>As usual, I did all the art in <a href="https://www.pyxeledit.com/" title="Pyxel Edit">Pyxel Edit</a>. I started off very rough with just pink boxes for the ships and a partial circle for the background, and moved immediately onto the gameplay with those placeholders. This is a somewhat unusual approach for me as I usually try to tie down the look of a game early on.</p>
<p><img alt="First mockup" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/FirstMockup.png"></p>
<p>The thing about rough mockups is that by the time I came back to the art a bunch of stuff in the game was already tied to the resolution and shape of the outlines. I wasn’t even sure I had done the perspective correctly, but I was stuck with it anyway, and I guess it was close enough because nobody has complained!</p>
<p><img alt="First ships" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/art_opt.jpg"></p>
<p>I had planned to do some character portraits as well for the narrative side of the game, and express encounters as dialogues between characters, but unfortunately there wasn’t time for that.</p>
<p>The map screen also didn’t get much love, with the initial placeholder art surviving into the final game.</p>
<p><img alt="Map screen" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/OOG_Map_Trail.jpg"></p>
<h2>Code</h2>
<p>As for my last Ludum Dare entry, <a href="https://blog.hyperlinkyourheart.com/gophers-post-mortem.html">Gophers</a>, a <a href="https://github.com/khoulihan/godot-cutscene-graph" title="Cutscene Graph Editor">cutscene graph editor plugin</a> for <a href="https://godotengine.org/" title="The game engine you waited for.">Godot</a> that I had previously developed was essential to getting this game done within the time constraints of the jam.</p>
<p><img alt="Those darn teenagers!" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/teenagers_dialogue.jpg"></p>
<p>I had done a lot of work on this plugin in recent months, applying lessons learned from using it for Gophers to improve the workflow, functionality, and stability. This paid huge dividends, and I found almost no issues while churning out encounter cutscenes. The cutscene graphs created by this plugin power every encounter in the game up to and after combat.</p>
<p>My main task on the first day was to get the map screen working - it wouldn’t be much of a game if you couldn’t travel between systems. I implemented it as a graph defined by custom <code>MapSystem</code> and <code>MapConnection</code> control nodes.</p>
<p><img alt="Map graph" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/map_ui.jpg"></p>
<p>The new fun thing about this for me was declaring the <code>MapConnection</code> class to be a “tool” script, allowing the connections between systems to be drawn in the editor. This kind of custom tooling is important for the efficient design of systems in larger games, so it was fun to give it a try. Here, it mostly served to prevent me from getting confused about which <code>MapSystem</code> nodes I had already connected to each other and which I hadn’t.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_draw</span><span class="p">():</span>
<span class="k">if</span> <span class="n">usable</span> <span class="ow">or</span> <span class="n">used</span> <span class="ow">or</span> <span class="n">_can_draw_in_editor</span><span class="p">():</span>
<span class="n">_load_nodes_for_editor</span><span class="p">()</span>
<span class="c1"># TODO: Decide on proper colours for this</span>
<span class="n">var</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">.</span><span class="n">gray</span>
<span class="k">if</span> <span class="n">usable</span><span class="p">:</span>
<span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">.</span><span class="n">hotpink</span>
<span class="c1"># No idea why the offset is required</span>
<span class="n">var</span> <span class="n">source_center</span> <span class="o">=</span> <span class="n">_source_node</span><span class="o">.</span><span class="n">rect_global_position</span> <span class="o">+</span> <span class="n">CENTER_OFFSET</span>
<span class="n">var</span> <span class="n">target_center</span> <span class="o">=</span> <span class="n">_target_node</span><span class="o">.</span><span class="n">rect_global_position</span> <span class="o">+</span> <span class="n">CENTER_OFFSET</span>
<span class="n">var</span> <span class="n">start</span> <span class="o">=</span> <span class="n">source_center</span> <span class="o">+</span> <span class="p">((</span><span class="n">target_center</span> <span class="o">-</span> <span class="n">source_center</span><span class="p">)</span><span class="o">.</span><span class="n">normalized</span><span class="p">()</span> <span class="o">*</span> <span class="n">SPACE</span><span class="p">)</span>
<span class="n">var</span> <span class="n">finish</span> <span class="o">=</span> <span class="n">target_center</span> <span class="o">+</span> <span class="p">((</span><span class="n">source_center</span> <span class="o">-</span> <span class="n">target_center</span><span class="p">)</span><span class="o">.</span><span class="n">normalized</span><span class="p">()</span> <span class="o">*</span> <span class="n">SPACE</span><span class="p">)</span>
<span class="n">draw_line</span><span class="p">(</span>
<span class="n">start</span><span class="p">,</span>
<span class="n">finish</span><span class="p">,</span>
<span class="n">color</span><span class="p">,</span>
<span class="n">LINE_THICKNESS</span><span class="p">,</span>
<span class="n">false</span>
<span class="p">)</span>
</code></pre></div>
<p>Implementing the combat was a lot tougher. The battle scene is almost entirely <span class="caps">UI</span> nodes, which was perhaps a mistake, and for some reason I struggled most of day two to even get player input to register properly. I don’t really remember now what the obstacle was to this - maybe I was just tired.</p>
<p><img alt="Battle UI" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/encounter_ui.jpg"></p>
<h2>Sound and Music</h2>
<p>There’s nothing special about the sound this time around. Unlike for Gophers I didn’t have time to do any foley work, and I figured I would probably have a hard time creating gunshot and explosion sounds anyway. Instead I fell back to <span class="caps">SFXR</span> (specifically, <a href="https://sfxr.me/" title="jsfxr">jsfxr</a>).</p>
<p>The music I put together in <a href="https://beepbox.co" title="BeepBox">BeepBox</a>, my favourite game jam tool for chiptune music these days due to its simplicity. I tend not to get as bogged down with it as I do with <a href="https://lmms.io/" title="LMMS"><span class="caps">LMMS</span></a>. I initially wanted to try to create something in the style of Modest Mouse, given the game’s inspiration, but it was hard to recreate a guitar sound. Instead I ended up with a simple arpeggiated chord progression over a bass drone that I hope feels kind of epic, kind of longing and lonesome, I dunno.</p>
<p>The battle music is a variation of the same tune but just swaps out one of the instruments, doubles up the drone, and compresses and spreads the arpeggios across more octaves for a more urgent and chaotic feeling. The two tracks play in sync throughout the game and just cut between each other as necessary.</p>
<h2>Abandoned Ideas</h2>
<p>As usual there were many ideas that I had to put aside due to time constraints. I had planned to include a small number of missiles which could be fired during combat with devastating effect, to allow the player to pull themselves back from the brink of defeat. I had also intended to have targetable/damageable systems on the ships, including injury to the characters, so that you could take out the enemy’s weapon to gain a reprieve from being fired upon, or their engines and reduce their chance to dodge. You can see the proposed <span class="caps">UI</span> of these mechanics in the initial mockup above. Finally, I had planned a system of weapon upgrades which almost made it in, but not quite.</p>
<p>On the art side, I had wanted to include a number of different backgrounds to improve the sense of travelling between systems.</p>
<p>A big regret that I have is that I wasn’t able to include the character portraits, and express the encounters primarily as dialogue between characters. I think this would have introduced a lot more personality to the narrative side of the game, as opposed to the third-person narration that I had to go with.</p>
<h2>Post-Jam</h2>
<p>I like this game a lot, and I feel it is one that could be polished up and rounded out into a complete experience with relative ease, so I am determined to do a post-jam release with some of the missing features described above, and some of the problems with the jam release resolved (such as the <span class="caps">RNG</span> sometimes producing the same encounter multiple times in a row). I have already begun work on this, and I hope to have it completed in the next few months. I’ll do another post about my progress on it so far, but for now here’s a little sneak peek:</p>
<p><a href="https://hyperlinkyourheart.itch.io/out-of-gas"><img alt="Haze speaks!" src="https://blog.hyperlinkyourheart.com/images/out-of-gas-post-mortem/HazeSpeaks.jpg"></a></p>Coroutine Callbacks2021-04-05T18:29:00+02:002021-04-05T18:29:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2021-04-05:/coroutine-callbacks.html<p>Using coroutines to call back to the emitter of a signal.</p><p>I’m working on a cutscene/dialogue graph editor plugin for the <a href="https://godotengine.org/" title="The game engine you waited for.">Godot game engine</a> (I mentioned using it for the Ludum Dare in a <a href="https://blog.hyperlinkyourheart.com/gophers-post-mortem.html">previous post</a>). When I came to deciding how to actually process the graphs it creates in order to display the dialogue and perform actions in the game, I discovered an interesting use for coroutines that I haven’t seen described elsewhere. I’m not all that used to working with coroutines so maybe what I’ve done is completely normal and everybody does it all the time, or indeed it might be ill-advised for some reason. Either way, I will describe it here and you can ignore me if I’m being an idiot!</p>
<p><a href="https://github.com/khoulihan/godot-cutscene-graph" title="Cutscene Graph Editor"><img alt="Graph editor" src="https://blog.hyperlinkyourheart.com/images/gophers-post-mortem/graph.png"></a></p>
<p>Processing a graph that results in activity in-game is a task which frequently needs to be put on hold - waiting for text to be displayed in the <span class="caps">UI</span> in a juicy manner, moving characters around, waiting for input etc. This could be achieved in a <code>Node</code> by implementing <code>_process()</code> and maintaining state about whether you’re currently walking a graph or waiting on something, and having methods that other nodes can use to indicate when processing can continue. I have found such methods to be quite messy in the past.</p>
<p>I decided instead to process the graph in a <code>while</code> loop and <code>yield</code> as necessary while other nodes perform tasks initiated by a number of signals. Where tasks are expected to take some time to complete, a coroutine state object is passed to the signal which the listener can use to indicate that processing should continue, or in some cases return values.</p>
<h2>Let’s See Some Code</h2>
<p>Below is an abridged version of the main method that processes the graphs. Processing a dialogue node yields so that the <span class="caps">UI</span> that displays the text can wait for player input to continue. Processing a branch node and others like it just proceed directly to the next iteration of the loop.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">process_cutscene</span><span class="p">(</span><span class="n">cutscene</span><span class="p">):</span>
<span class="n">_local_store</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">_current_graph</span> <span class="o">=</span> <span class="n">cutscene</span>
<span class="n">_current_node</span> <span class="o">=</span> <span class="n">_current_graph</span><span class="o">.</span><span class="n">root_node</span>
<span class="k">while</span> <span class="n">_current_node</span> <span class="o">!=</span> <span class="n">null</span><span class="p">:</span>
<span class="k">if</span> <span class="n">_current_node</span> <span class="ow">is</span> <span class="n">DialogueTextNode</span><span class="p">:</span>
<span class="k">yield</span><span class="p">(</span><span class="n">_process_dialogue_node</span><span class="p">(),</span> <span class="s2">"completed"</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">_current_node</span> <span class="ow">is</span> <span class="n">BranchNode</span><span class="p">:</span>
<span class="n">_process_branch_node</span><span class="p">()</span>
<span class="o">...</span>
</code></pre></div>
<p>Here is the code that processes dialogue nodes and raises the signal for their text to be displayed in-game. A coroutine is put in a frozen state to be passed with the signal, and the call to emit the signal is deferred so we can make sure we are waiting for the coroutine to complete before any signal listeners have a chance to resume it.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_await_response</span><span class="p">():</span>
<span class="k">return</span> <span class="k">yield</span><span class="p">()</span>
<span class="n">func</span> <span class="n">_emit_dialogue_signal</span><span class="p">(</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">character_name</span><span class="p">,</span>
<span class="n">character_variant</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">):</span>
<span class="n">emit_signal</span><span class="p">(</span>
<span class="s2">"dialogue_display_requested"</span><span class="p">,</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">character_name</span><span class="p">,</span>
<span class="n">character_variant</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">)</span>
<span class="n">func</span> <span class="n">_process_dialogue_node</span><span class="p">():</span>
<span class="n">var</span> <span class="n">text</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">text</span>
<span class="n">var</span> <span class="n">process</span> <span class="o">=</span> <span class="n">_await_response</span><span class="p">()</span>
<span class="n">call_deferred</span><span class="p">(</span>
<span class="s2">"_emit_dialogue_signal"</span><span class="p">,</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">_current_node</span><span class="o">.</span><span class="n">character</span><span class="o">.</span><span class="n">character_name</span><span class="p">,</span>
<span class="n">_current_node</span><span class="o">.</span><span class="n">character_variant</span><span class="o">.</span><span class="n">variant_name</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">)</span>
<span class="k">yield</span><span class="p">(</span><span class="n">process</span><span class="p">,</span> <span class="s2">"completed"</span><span class="p">)</span>
<span class="n">_current_node</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">next</span>
</code></pre></div>
<p>The code that consumes the signal can take however long it likes to do its thing, and when it’s ready it can just call <code>resume()</code> on the coroutine state and the graph processing will proceed to the next node.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_on_CutsceneController_dialogue_display_requested</span><span class="p">(</span>
<span class="n">text</span><span class="p">,</span>
<span class="n">character_name</span><span class="p">,</span>
<span class="n">character_variant</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">):</span>
<span class="n">_process</span> <span class="o">=</span> <span class="n">process</span>
<span class="o">...</span>
<span class="n">func</span> <span class="n">_on_ContinueButton_pressed</span><span class="p">():</span>
<span class="k">if</span> <span class="n">_process</span> <span class="o">!=</span> <span class="n">null</span><span class="p">:</span>
<span class="n">_process</span><span class="o">.</span><span class="n">resume</span><span class="p">()</span>
</code></pre></div>
<p>If a node in the graph needs a response in order to know what to do next (e.g. a choice between multiple dialogue options), we can just accept the return value of the coroutine passed in the signal. The consumer can just pass the value to the <code>resume()</code> call.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_process_choice_node</span><span class="p">():</span>
<span class="o">...</span>
<span class="n">var</span> <span class="n">process</span> <span class="o">=</span> <span class="n">_await_response</span><span class="p">()</span>
<span class="n">call_deferred</span><span class="p">(</span>
<span class="s2">"_emit_choices_signal"</span><span class="p">,</span>
<span class="n">choices</span><span class="p">,</span>
<span class="n">process</span>
<span class="p">)</span>
<span class="n">var</span> <span class="n">choice</span> <span class="o">=</span> <span class="k">yield</span><span class="p">(</span><span class="n">process</span><span class="p">,</span> <span class="s2">"completed"</span><span class="p">)</span>
<span class="n">_current_node</span> <span class="o">=</span> <span class="n">_current_node</span><span class="o">.</span><span class="n">branches</span><span class="p">[</span><span class="n">choice</span><span class="p">]</span>
</code></pre></div>
<h2>Potential Pitfalls</h2>
<p>There are two pitfalls that come to mind with this approach, but they’re not really any different than what could occur with a <code>_process()</code> based approach like the one I described above.</p>
<ol>
<li>Processing of another graph could be triggered while one is still in progress. If the previous one is yielding it would end up processing the new graph as well when it resumes. This is quite easy to avoid by just noping out of the method if there is already work in progress.</li>
<li>If there are multiple listeners connected to the signals there could be ambiguity about which one is responsible for resuming. If there are multiple resume calls, all but the first will result in a runtime exception.</li>
</ol>
<p>In the event that you actually <em>want</em> multiple listeners to report in before resuming, it might be possible to craft a different coroutine that takes the <a href="https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-get-signal-connection-list" title="get_signal_connection_list()">number of listeners</a> and yields until they have all resumed. I think it would look something like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_await_many</span><span class="p">(</span><span class="n">count</span><span class="p">):</span>
<span class="k">while</span> <span class="n">count</span> <span class="o">></span> <span class="mi">0</span><span class="p">:</span>
<span class="k">yield</span><span class="p">()</span>
<span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>
<span class="n">func</span> <span class="n">_await_many_responses</span><span class="p">(</span><span class="n">count</span><span class="p">):</span>
<span class="n">var</span> <span class="n">responses</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">while</span> <span class="n">count</span> <span class="o">></span> <span class="mi">0</span><span class="p">:</span>
<span class="n">responses</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="k">yield</span><span class="p">())</span>
<span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">responses</span>
</code></pre></div>
<p>I haven’t tested that though.</p>
<h2>Conclusion</h2>
<p>I don’t really have one, I just thought it was a neat pattern that doesn’t require the listeners to keep a reference to the node doing the processing or have any knowledge of it aside from the signals it emits. You can check out <a href="https://github.com/khoulihan/godot-cutscene-graph" title="Cutscene Graph Editor">the full code on GitHub</a> as it stands currently. I haven’t tested it in a game yet, but I will probably enter the <a href="https://ldjam.com/" title="Ludum Dare 48">Ludum Dare</a> later this month and I might get a chance to use it then. I will update afterwards whether it is a disaster or a triumph!</p>America Wins Again2020-12-21T20:27:00+01:002020-12-31T15:33:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-12-21:/queens-gambit.html<p>Or does it?</p><p><img alt="Chess lives here" src="https://blog.hyperlinkyourheart.com/images/queens-gambit/chesshall.jpg" title="Chess lives here"></p>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">This post contains spoilers for the <span class="caps">TV</span> show “The Queen’s Gambit”</span></p>
<p>I loved pretty much everything about The Queen’s Gambit, and as you’ve probably gathered if you’ve read any of my other posts, I don’t have much to say about things if I’m not complaining about them! <i class="far fa-laugh-squint body-icon"></i></p>
<p>What I do feel obligated to talk about, however, are the differences between this show and “For All Mankind” in terms of how they deal with the Soviet Union. I’ve <a href="https://blog.hyperlinkyourheart.com/for-all-mankind.html">written previously</a> about how “For All Mankind” re-imagines the space race such that the <span class="caps">USA</span> remains the underdog after several successful Soviet moon landings - erasing real Soviet accomplishments in favour of fictional ones, and providing an impetus for the <span class="caps">US</span> to include women in its space program, something that it didn’t do in reality until the 1980’s.</p>
<p>The Queen’s Gambit does not rewrite history to the same extent. The chess world was dominated by Soviet players in the 50s, 60s and beyond, and the show acknowledges that readily, with “The Russian”, Vasily Borgov, being Beth Harmon’s ultimate opponent. Her breakthrough somewhat reflects that of American prodigy Bobby Fischer, so it’s not unprecedented. However, where For All Mankind featured a number of real-life historical figures, all the competitors in The Queen’s Gambit are fictional.</p>
<p><img alt="Team USA" src="https://blog.hyperlinkyourheart.com/images/queens-gambit/teamusa.jpg" title="Team USA"></p>
<p>In a way, however, Beth’s real opponent is not Borgov or any of the other men she defeats on the road to face him, but herself, her emotional problems, and her addictions. The games she plays are almost entirely without malice, with just a touch of smug arrogance on occasion, and everybody she faces ends up with enormous admiration for her. This is especially true of the Soviet players she faces, who almost seem happier to have been beaten by her than they would have been to have won. What cold war political animosity is present comes mostly from her <span class="caps">CIA</span> handler, and is treated as a ridiculous, petty distraction by Beth. Doesn’t he know that there’s <em>chess</em> to be played??</p>
<p>For All Mankind, in contrast, has that animosity at its core. Its the motivation behind all of the American government’s actions in the show, though not necessarily of everybody at <span class="caps">NASA</span>, while the one Soviet character seems to validate their suspicions.</p>
<p>The Queen’s Gambit’s portrayal of the Soviet Union is actually extraordinarily sympathetic as a result of being viewed through the lens of chess enthusiasm. Whereas at home chess is a niche interest, when Beth arrives in the <span class="caps">USSR</span> she discovers that it is a national obsession. At home her prospects are probably akin to those of Benny Watts, the <span class="caps">US</span> chess champion before her - obscurity, and a dingy basement apartment at best. In the <span class="caps">USSR</span> she is mobbed by an adoring crowd after every match, which she plays in a dedicated chess hall instead of whatever spaces are available. She even has to adopt their strategy of cooperating during adjournments before she is able to achieve victory, with a team of all the chess friends she’s acquired on her journey advising her on how to approach the rest of the game - an apparent admission of the superiority of collective cooperation over competition and individualism.</p>
<p><img alt="Welcome home" src="https://blog.hyperlinkyourheart.com/images/queens-gambit/parkchesscrowd.jpg" title="Welcome home"></p>
<p>The final scene sees Beth slipping her handler’s grasp on the way to the airport. She wanders the streets of Moscow unmolested to find the old guys playing chess in the park, and they greet her warmly before inviting her to play. There’s a distinct sense that she has found a home here, where the game she loves is played openly in the park instead of hidden away in the basement, as in the orphanage where she grew up.</p>
<p>Although this series is still about an American character and mostly set in America, it is much closer to being the view from “the other side” that I hoped for in my previous post.</p>Cybercentrism2020-12-12T19:26:00+01:002020-12-13T18:10:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-12-12:/cybercentrism.html<p>Ready Player One successfully merges cyberpunk with centrist politics, for some reason.</p><p><img alt="You're a wizard Wade!" src="https://blog.hyperlinkyourheart.com/images/cybercentrism/pass2.jpeg" title="You're a wizard Wade!"></p>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">This post contains spoilers for the movie “Ready Player One”</span></p>
<p>I finally gave in and watched Ready Player One, prompted by seeing the Internet dump on the sequel to the novel. I avoided it before now because the commentary on the novel made it sound like pandering nonsense, and I assumed the movie would be much the same. But, I wanted to see for myself.</p>
<p>Perhaps as a result of those lowered expectations, I actually found it quite entertaining on a surface level. The pop culture references were certainly omnipresent, but maybe not as tiresome as having them explained in writing would be. In movie form, a lot of them that are not plot-relevant just form a visual backdrop that actually help define a plausible metaverse, albeit one with a culture that is inexplicable stuck 30 years in the past.</p>
<p>But on the other hand, the centrality of pop culture to the plot, and why it is central, is also what leaves a bad taste in my mouth. In the universe of Ready Player One, the online world is dominated by a metaverse-like virtual reality game called the <span class="caps">OASIS</span>, a game owned and controlled by a single corporation, and ultimately a single man, its creator, James Halliday. When he dies, he decrees that his successor will be decided by a tripartite fetch quest within the virtual world. In a monumental act of narcissism, the quest’s challenges are not based on the skills required to run a massive company or a piece of essential infrastructure, or tests of morality or wisdom, but on intimate knowledge of his own life and his pop cultural obsessions. This might be an interesting setup if Halliday were the antagonist, but he’s not, he’s practically worshipped by the users of the <span class="caps">OASIS</span>.</p>
<h2>Wow!! Cool Future!!</h2>
<p>This movie has the trappings of cyberpunk, but little of the critique. Victory in this story is not the smashing of a corporate monopoly or frustrating of its goals, but the passing of the reins of power from a virtuous benevolent founder to a handful of virtuous disciples, through an absurd faux-meritocratic process where merit means liking the right books and games and movies.</p>
<p>The antagonist, Sorrento, is the <span class="caps">CEO</span> of a company that runs debtor’s prisons, yet somehow the movie gives the impression that his greater sin is being a poseur who’s only pretending to “get” the stuff that Halliday liked. The denouement even reveals that Halliday could have used his monopoly power to end the practice of indentured servitude at any time, but apparently chose not to, or just didn’t think to, and this is not framed as a critique of monopoly capitalism, but is just a throwaway line to demonstrate how benevolent the protagonist is in his new role.</p>
<p><img alt="Remember Tracer? Remember Chun-Li? Remember various skeletons?" src="https://blog.hyperlinkyourheart.com/images/cybercentrism/tracer.jpeg" title="Remember Tracer? Remember Chun-Li? Remember various skeletons?"></p>
<p>The third act involves an epic battle in the <span class="caps">OASIS</span>, with the user-base coming together to fight Sorrento’s forces and prevent them from completing the quest. It’s presented as an empowering grass-roots uprising to save the <span class="caps">OASIS</span> from an evil corporation, and fine, they are pretty evil… But the status quo is also intolerable. They’re not fighting for their own empowerment, but merely to keep the unaccountable corporate power that dominates their lives out of even more abusive hands. It’s a pretty uninspiring cause.</p>
<p>The movie’s true feelings on the nature of power, and the relationship of ordinary people to it, is revealed most starkly at its climax. The conflict in the <span class="caps">OASIS</span> has spilled over into the real world, with our intrepid heroes being chased through the streets in a van, exposed at every turn by <span class="caps">CCTV</span> tracking technology and drones. While Sorrento sets out to confront them personally, they put out a call for aid, for their supporters to gather in the slums and defend them. When Sorrento reaches them, a huge crowd of people stream out of the stacks to oppose him.</p>
<p><img alt="It's dangerous to go alone! Take this!" src="https://blog.hyperlinkyourheart.com/images/cybercentrism/takethis.jpeg" title="It's dangerous to go alone! Take this!"></p>
<p>For a moment, it seems as though the plot really is going to be resolved by collective direct action. <em>People power, woo!</em> Instead, Sorrento pulls out a gun, and the crowd parts. They could easily disarm him, there are hundreds of them all around him. Instead they gawp uselessly at him, right up to the point where he is threatening to shoot some children in the face. It is only the police arriving that prevents him from committing murder right in front of them.</p>
<p>With Sorrento out of the way thanks to the police, a bunch of lawyers arrive to certify the protagonist’s completion of the quest and acquisition of complete control over the <span class="caps">OASIS</span>. The crowd are reduced to placid spectators in a boardroom drama that doesn’t involve them or empower them. This is, I feel, the perfect encapsulation of the movie’s worldview. The crowd are the audience - expected to worship and trust benevolent CEOs, to react strongly in defence of a commodified culture where they are otherwise passive consumers, but to look on silently and not interfere as control over that culture changes hands, maintaining the status quo.</p>Avocados2020-11-02T21:23:00+01:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-11-02:/avocados.html<p>A couple of juicy avocados.</p><p><a href="https://portfolio.hyperlinkyourheart.com/avocados.html"><img alt="Avocados" src="https://blog.hyperlinkyourheart.com/images/avocados/Avocado02_x1.png" title="Avocados"></a></p>
<p>I’m not very good at doing these art posts in a timely manner; I did this still-life of a couple of avocados for pixel dailies back in September!</p>
<h2>Timelapse</h2>
<p><a href="https://www.youtube.com/watch?v=QP35xoMXE8U"><img alt="Avocados" src="https://img.youtube.com/vi/QP35xoMXE8U/0.jpg"></a></p>Mine2020-09-26T23:29:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-09-26:/mine.html<p>A portrait of my friend Mine.</p><p><a href="https://portfolio.hyperlinkyourheart.com/mine.html"><img alt="Mine" src="https://blog.hyperlinkyourheart.com/images/mine/Mine_270x_x1.png" title="Mine"></a></p>
<p>A portrait I did of my friend <a href="https://www.instagram.com/flamenclorca/">Mine</a> a few weeks ago.</p>
<h2>Timelapse</h2>
<p><a href="https://www.youtube.com/watch?v=rbd8nfxpNxY"><img alt="Mine" src="https://img.youtube.com/vi/rbd8nfxpNxY/0.jpg"></a></p>Anticipation2020-09-09T12:19:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-09-09:/anticipation.html<p>A dog waiting for a very important letter.</p><p><a href="https://portfolio.hyperlinkyourheart.com/anticipation.html"><img alt="Anticipation" src="https://blog.hyperlinkyourheart.com/images/anticipation/AnticipationPortrait11Animated_x1_Optimised.gif" title="Anticipation"></a></p>
<p>I was inspired to create this when I won a postcard and sticker pack giveaway on twitter by my friend <a href="https://twitter.com/moertel/">Stefanie</a>, and I was eagerly waiting for it to arrive! I actually did this a while ago but I forgot to post it here.</p>
<p><img alt="Postcard and stickers" src="https://blog.hyperlinkyourheart.com/images/anticipation/pack.jpg" title="Postcard and stickers"></p>
<p>Go check out Stefanie’s work, <a href="https://moer.tel/">it’s amazing</a>. Thanks for the goodies Stef!</p>
<h2>Timelapse</h2>
<p><a href="https://www.youtube.com/watch?v=IdL1mG59ApU"><img alt="Anticipation" src="https://img.youtube.com/vi/IdL1mG59ApU/0.jpg"></a></p>Stars Trek - Failures of Imagination2020-09-08T23:29:00+02:002020-09-08T23:29:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-09-08:/picard.html<p>Wherein I take Star Trek: Picard far too seriously.</p><p><img alt="Where did it all go wrong?" src="https://blog.hyperlinkyourheart.com/images/picard/picard-at-the-bar.jpg" title="Where did it all go wrong?"></p>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">This post contains spoilers for the <span class="caps">TV</span> show “Picard”</span></p>
<p>Star Trek: The Next Generation, and its follow-ups, Deep Space 9 and Voyager, are some of my favourite <span class="caps">TV</span> shows. They are set in a future post-scarcity utopia where poverty, disease and war (amongst humans and <em>most</em> of the alien species they encounter, at least) have been largely eliminated.</p>
<p>Although they sometimes deal with deep topics, like the humanity or personhood of artificial intelligences, the economic reality of their universe and how it came about goes largely unexamined. There is no money, we are told (except sometimes, when there is). There is property, apparently, but whether it works the same way as property does today is not discussed. Abundant energy and the technology to conjure most of the essentials of life from thin air mean that nobody goes hungry, but whether everybody can have the opportunity to own a vinyard in France without inheriting one is a question that goes unasked.</p>
<p>The utopianism is usually established instead through a gentle mocking of the preoccupations and vices of present-day society, or lecturing about how humanity has moved past them. A bemused Picard takes a cigarette offered by a 20th century character on the holodeck, and coughs aggressively. Imagine smoking! Even the Ferengi, themselves caricatures of grubby, grasping capitalists, are shocked at our stupidity when they learn about tobacco. Janeway lectures the omnipotent Q about resolving conflicts with diplomacy instead of violence. An arrogant 20th century revival is revealed to be a buffoon when he demands to be allowed to speak to his lawyer. No lawyers here sir - this is the lawyerless utopia of Lionel Hutz’s nightmares.</p>
<p>Occasionally the admonishment is more direct - such as when Sisko time travels to a ghetto in a North American city, in our near future, looks directly into the camera, and says “sort your fucking shit out”.</p>
<p>The point is that in the Star Trek future, society is better than it is today, but its material basis is vague to the point of absurdity. It’s unfortunate, because that would be an interesting topic to explore, but it is what it is - light-hearted science-fantasy more concerned with the personal growth of characters than with the economic basis of their society.</p>
<h2>A Shallow Dystopia</h2>
<p><img alt="Sacre bleu!" src="https://blog.hyperlinkyourheart.com/images/picard/picard-in-disguise-cropped.jpeg" title="Sacre bleu!"></p>
<p>Star Trek: Picard, the latest attempt at continuing the Star Trek franchise on television, is set, in contrast to its predecessors, in a fractured, broken society. The Federation has turned inward, failing to live up to its values of diplomatic and humanitarian outreach. Money has returned with a vengeance, and nobody does anything unless they’re getting paid. There’s a disaffected, under-appreciated working class, who apparently don’t even get the same quality of replicators as other sections of society. Where characters in other series’ have interests and hobbies, in Picard they have vices, addictions and psychological damage. One character constantly has a fat cigar hanging out of his mouth and nobody is shocked about it because he looks, just, <em>so cool</em>.</p>
<p>In a show without an established universe, these aspects would be unremarkable - just a different set of assumptions about human nature and the future development of society, and with different stories to tell. In Picard, the departure from the previous utopianism is not examined, much less explained, and it is jarring.</p>
<p>But the above examples are trivialities compared to a problem that is at the very heart of the show. Toiling alongside the human workers are a class of sentient android slaves whose <em>abolition and genocide</em> by the Federation serves as a major plot motivator.</p>
<h2>More than a Fistful</h2>
<p><img alt="Seriously bro?" src="https://blog.hyperlinkyourheart.com/images/picard/data-side-eye.jpg" title="Seriously bro?"></p>
<p>These are the “Datas on every starship” that Maddox expresses a desire for in the <span class="caps">TNG</span> episode “The Measure of a Man”. In that episode (one of the best in the Star Trek canon), Picard defends Data’s personhood and his right to self-determination. He’s ready to sacrifice his career over the possibility that his society would view even <em>one</em> android as property. In Picard, the same character disagrees with the ban on “synthetics”, but hardly comments on their genocide, or their previous condition of slavery. He quits Starfleet over their failure to provide humanitarian aid to the Romulans, not over the fact that the Federation had somehow come to rely on slave labour. Apparently, he would be perfectly happy to return to a situation that he fought to prevent when he was Captain of the Enterprise.</p>
<p>It’s a disappointing missed opportunity. It seems to me that the show wanted to say something about the socio-political situation in America today, but utterly fails to understand that situation, the shows that came before it, or the character of Picard.</p>
<p>If it really wanted to portray the dissolution of a utopia, a crumbling society betraying its ideals, it could have done so by clarifying the nature of the Federation economy, and provided some systemic explanation for the introduction of slave labour, money, and inequality, where those things did not exist before, or at least some believable political force pushing for those things. Picard could have been cast in the role of defending the fundamental rights of the Androids from a society that is determined to exploit them, as he has many times in the past. Perhaps he could even have taken on the new role of defending the rights of human labourers, whatever the reason that they’re suddenly being disenfranchised. It could have been a great opportunity to introduce some economic depth to the Star Trek universe that has long been lacking.</p>
<p>Instead, we get a complete mess where exploitation is ignored, the dissolution of Federation society is apparently due to infiltration by an inherently sinister other, and the androids are the ultimate villain for not being sympathetic to their oppressors or being understanding about the genocide of their race.</p>Observatory2020-07-02T18:48:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-07-02:/observatory.html<p>An animation for Asteroid day 2020.</p><p><a href="https://www.redbubble.com/shop/ap/51580357"><img alt="Observatory" src="https://blog.hyperlinkyourheart.com/images/observatory/ObservatoryComplete_x1_Optimised.gif" title="Observatory"></a></p>
<p>An animation I made for <a href="https://en.wikipedia.org/wiki/Asteroid_Day">Asteroid day</a> to continue my space-themed art streak! Thanks to my friend <a href="https://moer.tel/">Stefanie</a> for the suggestion! Please click that link and check out her absolutely stunning art, you won’t regret it.</p>
<h2>Prints</h2>
<p>Prints are available through <a href="https://www.redbubble.com/shop/ap/51580357">RedBubble</a>, <a href="https://www.inprnt.com/gallery/hyperlinkyourheart/pixel-art-observatory/"><span class="caps">INPRNT</span></a> and coming soon to Displate.</p>
<h2>Timelapse</h2>
<p><a href="https://youtu.be/Obh2zpAVn6k"><img alt="Observatory Timelapse" src="https://img.youtube.com/vi/Obh2zpAVn6k/0.jpg" title="Pixel Art Timelapse - Observatory"></a></p>Celestial2020-06-24T22:55:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-06-24:/celestial.html<p>Several pieces inspired by the pixel dailies theme “Celestial”.</p><p><a href="https://www.redbubble.com/shop/ap/50838959?asc=u"><img alt="Celestial 1" src="https://blog.hyperlinkyourheart.com/images/celestial/celestial1.png" title="Celestial 1"></a></p>
<p>Only a few days after the last inspiring pixel dailies prompt, another one appeared that I couldn’t resist - “Celestial”. I had three ideas for it immediately but I only had time to do the one above on the day. I’m glad I spaced them out anyway because I think I achieved a lot with the extra time.</p>
<p><a href="https://www.redbubble.com/shop/ap/50839684?asc=u"><img alt="Flyby" src="https://blog.hyperlinkyourheart.com/images/celestial/flyby.gif" title="Flyby"></a></p>
<p>The second idea I worked on was originally only a planet-rise over some mountains, but it evolved quite a bit as I worked on it until it was about spaceships racing over a peaceful alien village.</p>
<p>I’m quite pleased with the palette and animation on this one.</p>
<p><a href="https://www.redbubble.com/shop/ap/50840685?asc=u"><img alt="Celestial 2" src="https://blog.hyperlinkyourheart.com/images/celestial/celestial2.gif" title="Celestial 2"></a></p>
<p>The final concept I only completed yesterday, a view of the night sky through some trees.</p>
<h2>Prints</h2>
<p>Prints are available through <a href="https://randomhumanity.redbubble.com">RedBubble</a> (linked individually above), and <a href="https://displate.com/hyperlinkyourheart/celestial">Displate</a>.</p>
<h2>Timelapses</h2>
<p><a href="https://www.youtube.com/watch?v=bn12CoFhqLc"><img alt="Celestial 1" src="https://img.youtube.com/vi/bn12CoFhqLc/0.jpg"></a></p>
<p><a href="https://www.youtube.com/watch?v=xrv-JyAFfXA"><img alt="Flyby" src="https://img.youtube.com/vi/xrv-JyAFfXA/0.jpg"></a></p>
<p>Available from 2020-06-25:</p>
<p><a href="https://www.youtube.com/watch?v=Dn3sIkE6d3w"><img alt="Celestial 2" src="https://img.youtube.com/vi/Dn3sIkE6d3w/0.jpg"></a></p>Cosmic Eye2020-06-06T21:44:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-06-06:/cosmic-eye.html<p>A piece I did for pixel dailies on the theme “Eye”.</p><p><img alt="Cosmic Eye" src="https://blog.hyperlinkyourheart.com/images/cosmic-eye/SpaceEye_216_01_x1.png" title="Cosmic Eye"></p>
<p>I got back into the pixel dailies a couple of days this week. I’ve never used them as an actual daily practice, but when I see a theme I like I jump in. This day, the theme was “Eye”, and I <em>really</em> like eyes.</p>
<p>Unfortunately I was pushed for time that day so the result is a bit rough - however, I think it gets the idea across, which is “eyes which are full of stars and made of stars and also stars were there”.</p>
<p>I started this one out with my drawing tablet, which I don’t usually use for pixel art and amn’t very good with. I’m trying to get away from using so many rigid straight lines, and treat pixel art more like regular painting. I had to switch to the mouse towards the end for the finer details, but it’s a start.</p>
<h2>Timelapse</h2>
<p><a href="https://www.youtube.com/watch?v=ZwnhLEPTCOM"><img alt="Cosmic Eye" src="https://img.youtube.com/vi/ZwnhLEPTCOM/0.jpg"></a></p>Devs - Spirituality as a Service2020-05-18T17:56:00+02:002020-05-18T22:05:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-05-18:/devs.html<p>Beautiful, and perhaps appropriately predictable, but also frustrating in its conception of causality and the characters blinkered behaviour.</p><p><img alt="Subtle" src="https://blog.hyperlinkyourheart.com/images/devs/forest.jpg" title="Subtle"></p>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">This post contains spoilers for the <span class="caps">TV</span> show “Devs”</span></p>
<p>I liked <a href="https://www.imdb.com/title/tt8134186/?ref_=nv_sr_srsg_0">Devs</a> a lot. It looks at the quasi-religious reverence in which tech entrepreneurs are held in some quarters (most notably amongst themselves, perhaps) and asks, what if this but literally? What if these people were literally gods, or creating a god?</p>
<p>The plot centres on a software engineer named Lily, whose boyfriend is murdered by their boss, Forest, after he attempts to steal some code from the company they work for. The code in question is for the Devs system - a quantum simulator that extrapolates the past and future events of the entire universe from any sample of matter. Lily becomes suspicious of the circumstances of her boyfriend’s death, which is made to look like a suicide, and starts to dig around.</p>
<p>Unfortunately much of the plot, and particularly the climax, rest on a concept that I found it hard to suspend my disbelief about (and I don’t mean the premise of the Devs system).</p>
<p>Several of the main characters are aware of future events, up to a certain point, thanks to their quantum computer’s simulations. They do not attempt to alter their behaviour in even the smallest way, even just to see if it is possible, instead slavishly repeating every word and action they’ve observed.</p>
<p>If it were just Forest, and the lead systems designer, Katie, who acted like this, it might be understood as a consequence of blind faith, or a wilful misunderstanding of causality because reality doesn’t suit their purposes. Forest is single-minded in his pursuit of this technology because he believes it can resurrect his dead daughter - Devs is his church, determinism is the creed, and anything that calls it into question is heresy.</p>
<p>But this notion is dispelled in a scene where a roomful of people are shown a simulation of a few seconds into the future, and mirror it exactly - apparently it is actually a feature of this universe that it is actively difficult to behave contrary to the prediction. I think the reality would be the opposite - it would actually be difficult <em>not</em> to act differently once you were aware of future events. I think you would do so instinctively, and accidentally. It wouldn’t be a violation of causality, because the simulation would also be a cause, with its own effects.</p>
<p>So this concept strains credibility, and works only on a allegorical level - the low-level developers are dazzled by a brief tech demo and its promises while the higher ups are simultaneously in thrall to their own hype and aware of the lies it is based on and the limits of their knowledge.</p>
<p>It also makes the climax of the show absurdly predictable. As soon as we hear that the simulation breaks down at a certain point, and it has something to do with Lily, we know that Lily is going to do something that contradicts the predictions of the simulation. None of the supposedly smart characters in the show demonstrate any awareness of this obvious fact, and it’s frustrating. It is only redeemed because seeing the climax coming reflects the characters’ foreknowledge of the future, in a way.</p>
<p><img alt="Lily" src="https://blog.hyperlinkyourheart.com/images/devs/lily_reflection.jpg" title="Lily doing some reflecting"></p>
<p>Overall, it’s interesting enough and well enough written that these problems are easy to look past. Some of the imagery is fantastic, such as the would-be god-developers working in a giant fractal computer floating in a vacuum, completely isolated from the world they’re trying to understand. It’s also a tonal masterpiece, full of haunting establishing shots, temple-like sets, and an unsettling soundtrack. Worth watching for that reason alone, to be honest.</p>Ludum Dare 46 Results2020-05-13T22:34:00+02:002020-05-13T23:58:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-05-13:/gophers-results.html<p>Results for Ludum Dare 46 are out, and I did pretty well!</p><p>The Ludum Dare 46 results were published yesterday, and <a href="https://hyperlinkyourheart.itch.io/gophers">my game</a> did quite well, placing 109th overall and 14th in the “Mood” category, as well as 120th and 121st in graphics and audio respectively. In the largest ever Ludum Dare, those are pretty decent placings I think, despite not breaking the top 100.</p>
<table>
<thead>
<tr>
<th style="text-align: left;">Category</th>
<th style="text-align: right;">Rating</th>
<th style="text-align: right;">Placing</th>
<th style="text-align: right;">Percentile</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Overall</td>
<td style="text-align: right;">4.136</td>
<td style="text-align: right;">109</td>
<td style="text-align: right;">96th</td>
</tr>
<tr>
<td style="text-align: left;">Fun</td>
<td style="text-align: right;">3.523</td>
<td style="text-align: right;">819</td>
<td style="text-align: right;">77th</td>
</tr>
<tr>
<td style="text-align: left;">Theme</td>
<td style="text-align: right;">4.14</td>
<td style="text-align: right;">279</td>
<td style="text-align: right;">92nd</td>
</tr>
<tr>
<td style="text-align: left;">Innovation</td>
<td style="text-align: right;">3.86</td>
<td style="text-align: right;">247</td>
<td style="text-align: right;">93rd</td>
</tr>
<tr>
<td style="text-align: left;">Humor</td>
<td style="text-align: right;">3.656</td>
<td style="text-align: right;">365</td>
<td style="text-align: right;">89th</td>
</tr>
<tr>
<td style="text-align: left;">Graphics</td>
<td style="text-align: right;">4.477</td>
<td style="text-align: right;">120</td>
<td style="text-align: right;">96th</td>
</tr>
<tr>
<td style="text-align: left;">Audio</td>
<td style="text-align: right;">4.102</td>
<td style="text-align: right;">121</td>
<td style="text-align: right;">96th</td>
</tr>
<tr>
<td style="text-align: left;">Mood</td>
<td style="text-align: right;">4.523</td>
<td style="text-align: right;">14</td>
<td style="text-align: right;">99th</td>
</tr>
</tbody>
</table>
<h2>Graphs</h2>
<p>I always feel that the real competition in the Ludum Dare is against myself - just trying to do a little bit better and learn a bit more each time. As such, here’s some indication of my <span class="caps">LD</span> result trends over the years.</p>
<p><img alt="Ratings Graph" src="https://blog.hyperlinkyourheart.com/images/gophers-results/ratings_ogm.png">
<img alt="Placings Graph" src="https://blog.hyperlinkyourheart.com/images/gophers-results/placings_ogm.png">
<img alt="Percentiles Graph" src="https://blog.hyperlinkyourheart.com/images/gophers-results/percentiles_ogm.png"></p>
<p>Nice upward trends! Note that I was only responsible for the art for “Claustrophobia” and “Rattendorf”, so I can only take partial credit for the overall and mood ratings of those.</p>
<p>The real learning experience this time around was on the audio. I’ve only done the audio for six of the nine Ludum Dares I’ve entered, so I left it out of the graphs above.</p>
<p><img alt="Ratings Graph" src="https://blog.hyperlinkyourheart.com/images/gophers-results/ratings_audio.png"></p>
<p>Looks like I really cranked it up a notch this time after coasting for a long while. Nice.</p>
<h2>Moar Gophers</h2>
<p>I haven’t decided yet if I’m going to take the game further. I quite like the concept and I certainly have some ideas for it. I’ll probably finish off my <a href="https://github.com/khoulihan/gopher-render">gopher renderer</a> and phlog generator before I decide, and then I can do a <em>devphlog</em> for it :D</p>
<p>You can still play the <a href="https://hyperlinkyourheart.itch.io/gophers">jam version</a> for now, if you missed it.</p>Gophers2020-04-25T17:24:00+02:002021-05-30T17:25:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-04-25:/gophers-post-mortem.html<p>A post-mortem of my Ludum Dare 46 entry, Gophers - a short adventure game about keeping a gopher network alive after some sort of nuclear event has destroyed civilisation.</p><p><a href="https://hyperlinkyourheart.itch.io/gophers"><img alt="Overlooking the city" src="https://blog.hyperlinkyourheart.com/images/gophers-post-mortem/Gophers_tower.jpg"></a></p>
<p><a href="https://hyperlinkyourheart.itch.io/gophers" title="Gophers">Gophers</a> is my entry for <a href="https://ldjam.com/events/ludum-dare/46" title="Ludum Dare 46">Ludum Dare 46</a>, the most recent of the bi-annual Ludum Dare game jams. It is a short adventure game about maintaining a <a href="https://en.wikipedia.org/wiki/Gopher_(protocol)" title="Gopher Protocol">gopher</a> network in a post-apocalyptic world.</p>
<p>The basic concept is one I’ve been kicking around for a while as a sort of casual <span class="caps">RPG</span>/survival game about maintaining computer networks on scavenged technology, so it came to mind immediately when I saw the theme (“Keep it alive”).</p>
<p>I’ve been really interested lately in gopher and other low-overhead technologies, and what the internet would look like if the industries that sustain it collapsed. I’d previously envisioned a relatively cheerful solarpunk game about connecting distant sustainable communities, but I think it took on a much darker tone because of recent events.</p>
<h2>Art</h2>
<p>I did all the art in <a href="https://www.pyxeledit.com/" title="Pyxel Edit">Pyxel Edit</a> as usual. My goal was to keep everything abstract and as high-contrast and readable as possible while still allowing for a nice parallax cityscape. I started with a mock-up of the exterior scene, and then essentially flipped the background and foreground colours from that for the bunker scene. I only used 7 colours in the end.</p>
<p><img alt="Bunker Scene" src="https://blog.hyperlinkyourheart.com/images/gophers-post-mortem/BunkerScreenie.jpg"></p>
<p>I put together a timelapse of the art so you can see the whole process:</p>
<p><a href="https://www.youtube.com/watch?v=0jPLMCfSE0w"><img alt="Gophers" src="https://img.youtube.com/vi/0jPLMCfSE0w/0.jpg"></a></p>
<h2>Code</h2>
<p>The only reason why I considered this a viable idea was because I had previously developed a <a href="https://github.com/khoulihan/godot-cutscene-graph" title="Cutscene Graph Editor">cutscene graph editor plugin</a> for <a href="https://godotengine.org/" title="The game engine you waited for.">Godot</a>. It was untested in any game but I thought it would give me enough of a leg up that I would have time for the art and writing.</p>
<p><a href="https://github.com/khoulihan/godot-cutscene-graph" title="Cutscene Graph Editor"><img alt="Graph editor" src="https://blog.hyperlinkyourheart.com/images/gophers-post-mortem/graph.jpg"></a></p>
<p>So in effect, the “gopher network” in the game is actually a dialogue tree!</p>
<p>Actually using the editor in a game did reveal some issues with it, but nothing significant enough to prevent me from finishing - and now I have some ideas on what needs work before I use it for another game!</p>
<p>I also took some code from a <a href="https://hyperlinkyourheart.itch.io/people-poker" title="People Poker">previous game of mine</a> for doing the menus and dealing with the settings. Every bit helps when you’re entering the jam solo.</p>
<p>One thing that really came together for me in this jam was using coroutines to manage sequences of events. I’ve always struggled to wrap my head around them previously for some reason, and would clumsily hook up signal handlers for every step. Using the <code>yield</code> statement in <a href="https://godotengine.org/" title="The game engine you waited for.">Godot</a> made handling interactions much easier and quicker to write.</p>
<div class="highlight"><pre><span></span><code><span class="n">func</span> <span class="n">_on_Terminal_clicked</span><span class="p">(</span><span class="n">walk_target</span><span class="p">,</span> <span class="n">face_direction</span><span class="p">):</span>
<span class="n">_player</span><span class="o">.</span><span class="n">set_destination</span><span class="p">(</span><span class="n">walk_target</span><span class="p">)</span>
<span class="k">yield</span><span class="p">(</span><span class="n">_player</span><span class="p">,</span> <span class="s2">"arrived_at_destination"</span><span class="p">)</span>
<span class="n">_player</span><span class="o">.</span><span class="n">face</span><span class="p">(</span><span class="n">face_direction</span><span class="p">)</span>
<span class="n">GameController</span><span class="o">.</span><span class="n">set_spawn_location</span><span class="p">(</span><span class="s2">"bunker"</span><span class="p">,</span> <span class="s2">"terminal"</span><span class="p">)</span>
<span class="n">GameController</span><span class="o">.</span><span class="n">set_spawn_direction</span><span class="p">(</span><span class="s2">"bunker"</span><span class="p">,</span> <span class="s2">"right"</span><span class="p">)</span>
<span class="n">FadeMask</span><span class="o">.</span><span class="n">fade_in</span><span class="p">()</span>
<span class="k">yield</span><span class="p">(</span><span class="n">FadeMask</span><span class="p">,</span> <span class="s2">"fade_in_complete"</span><span class="p">)</span>
<span class="c1"># Switch to the browser scene</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_tree</span><span class="p">()</span><span class="o">.</span><span class="n">change_scene</span><span class="p">(</span><span class="s2">"res://browser/Browser.tscn"</span><span class="p">)</span>
<span class="n">FadeMask</span><span class="o">.</span><span class="n">fade_out</span><span class="p">()</span>
<span class="k">yield</span><span class="p">(</span><span class="n">FadeMask</span><span class="p">,</span> <span class="s2">"fade_out_complete"</span><span class="p">)</span>
</code></pre></div>
<h2>Sound Effects</h2>
<p>The most exciting part of working on this game, for me, was doing the sound effects. I bought a fancy mic a while back (a <a href="https://www.rode.com/microphones/nt-usb" title="Røde NT-USB">Røde <span class="caps">NT</span>-<span class="caps">USB</span></a>) to do foley <span class="caps">SFX</span> rather than my usual <span class="caps">SFXR</span> beeps and boops, but this was the first chance I’ve had to try it out.</p>
<p><img alt="My foley kit, or part of it at least" src="https://blog.hyperlinkyourheart.com/images/gophers-post-mortem/foley-kit.jpg"></p>
<p>For the Geiger counter sounds I ran my finger over the teeth of a comb. For the bunker door, I rubbed a hammer and a spanner together in various ways. For the dripping sound in the bunker, I just used an eyedropper to drip drops into a glass of water. The footsteps are real footsteps that I recorded, and the cloth sounds when you’re walking around the exterior are me crinkling a vinyl jacket. It was a lot of fun to record all these and I don’t think I was even being all that creative. I couldn’t figure out how to do buzzing or flickering sounds for the electric light within the time I had though, unfortunately.</p>
<p>One big problem I encountered was that my apartment is apparently incredibly noisy, as am I. It was a windy day and the shutters on my window were banging constantly, my neighbours were going about their noisy lives, oblivious, and my body stubbornly refused to go without oxygen during the recordings. Noise reduction in <a href="https://www.audacityteam.org/" title="Audacity audio editor">Audacity</a> helped a bit (make sure you record periods of “silence” to enable this), but there are definitely some extra environmental sounds in there. Thankfully I think they mostly just appear as mysterious underground reverb or get buried by other things. It’s something I’m definitely going to have to think about for next time.</p>
<p>I did a bunch of post-processing in <a href="https://www.audacityteam.org/" title="Audacity audio editor">Audacity</a> to pick the best bits out of the recordings, and make things sound better. I had to reduce the pitch on the bunker door sound to make it sound heavier, for example.</p>
<h2>Music</h2>
<p>I was so proud of the sound effects that I almost wasn’t going to do any music, but I’m glad I did. I got to it in the last few hours of the jam, so I had to keep it very simple. It’s mostly just the notes of a Dmin7 chord played in a few different arrangements on pad instruments, with some slow bass drums coming in and out. The title screen music layers a couple of different pads as well as a Rhodes doing sus4 arpeggios from each note of the chord.</p>
<p>I put everything together in <a href="https://lmms.io/" title="LMMS"><span class="caps">LMMS</span></a>. I spent a good chunk of time experimenting with different instruments so even though it’s really minimalistic it still took a while!</p>
<h2>Abandoned Ideas</h2>
<p>I had planned several other game elements, including the protagonist saying things to himself (or the player), and another type of interaction involving connecting cables and swapping out computer components.</p>
<p>A full game would probably have more complex survival elements instead of a simple timer, and would see you having to scavenge in the environment for computer equipment and other supplies.</p>
<p>We’ll see if anything like that comes to fruition in the future!</p>For All Mankind2020-04-14T16:48:00+02:002020-05-17T19:09:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-04-14:/for-all-mankind.html<p>America wins, even when it loses.</p><p><a href="https://pixabay.com/vectors/basic-desolate-flag-old-russia-1299705/"><img alt="Red Moon" src="https://blog.hyperlinkyourheart.com/images/for-all-mankind/soviet-flag.jpg"></a></p>
<p><i class="fas fa-exclamation-triangle spoiler-icon"></i><span class="spoiler-text">This post contains spoilers for the <span class="caps">TV</span> show “For All Mankind”</span></p>
<p><span class="dquo">“</span>For All Mankind” is a strange show. It reimagines the space race of the late 1960s in such a way that the <span class="caps">USA</span> is the underdog, with the <span class="caps">USSR</span> beating them to the moon by a month. While <span class="caps">NASA</span>’s failures are compounded by the crash-landing of the Apollo 11 lander, the Soviets rack up another victory when they land the first woman on the moon. Eventually the Americans get their act together and land a woman on the moon as well, and from that point on the two superpowers are neck and neck in space.</p>
<p>The strange thing about this is the extent to which it reflects reality, but just displaces it in time. The <span class="caps">USA</span> were playing catch-up for much of the space race, with the <span class="caps">USSR</span> achieving all the important early milestones: first artificial satellite, first animal in orbit, first human. The moon landing has so overshadowed those achievements in the popular consciousness that it is the only conceivable starting point for an alternate history like this. By giving it to the <span class="caps">USSR</span>, the moon landing becomes Sputnik.</p>
<p>The <span class="caps">USSR</span> did achieve another first of particular relevance to this show: they put the first woman into orbit, in 1963. Though female cosmonauts were not a permanent feature of the Soviet space program, female astronauts were not a part of the <span class="caps">US</span> space program at all, and they didn’t put a woman into space until 20 years later.</p>
<p>Interestingly, though the fictional Soviet moon landing featured an actual cosmonaut (Alexei Leonov, who conducted the first spacewalk in 1965), the female cosmonaut is not Valentina Tereshkova, the first woman in space, nor any of the women in her program, but a completely fictional character. The show has no problem giving a nod to Mercury 13 candidate Jerrie Cobb in the form of fictional Molly Cobb, but the Soviet women receive no such acknowledgement.</p>
<p>It’s not all bad. The premise feels like it is asking us to celebrate the <span class="caps">USA</span> for an egalitarianism that it never possessed, but the drama doesn’t necessarily reflect that. The women face opposition and scepticism as to their abilities - maybe not to the extent that they would have in reality, but it’s there. Gay characters have to live their lives in secret without any attempt to pretend that it could have been otherwise. America’s continued participation in the space race is unequivocally driven by militarism and suspicion. The Soviet cosmonauts even get a few humanising moments, but they are ultimately cast as a sinister other.</p>
<p>It is sad that even now, nearly three decades on from its collapse, the Soviet Union can only ever be condemned for its failures, never acknowledged for its accomplishments. I suppose this show goes further than most in that regard, but it maintains an unquestionably American perspective, with fictional Soviet victories serving merely to encourage America on to even greater heights. It would be nice to see something from the other side some time.</p>Embedding SVGs in Pelican2020-04-04T00:10:00+02:002020-04-04T00:10:00+02:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-04-04:/embedding-svgs.html<p>A static alternative to Font Awesome’s dynamic icon embedding.</p><p>In my <a href="https://blog.hyperlinkyourheart.com/remember-blogs.html">inaugural post</a> I mentioned that one problem I had encountered while designing this blog was styling the <span class="caps">SVG</span> icons. I had grabbed a bunch of the individual icon files from <a href="https://fontawesome.com/">Font Awesome</a>, but because of the way SVGs, <span class="caps">CSS</span> and <span class="caps">HTML</span> interact, I wasn’t able to colour them directly using <span class="caps">CSS</span> <code>color</code> or <code>fill</code> properties, and instead had to use <code>filter</code> properties (which I calculated using <a href="https://codepen.io/sosuke/pen/Pjoqqp">this tool</a>, so it wasn’t too much of a hardship).</p>
<p>I also didn’t particularly like that retrieving the icons involved numerous separate requests, nor the visible “pop-in” in Firefox that resulted from having them referenced as external files. The files are tiny, with the request overhead often as large or larger than the files themselves.</p>
<p>A further advantage that I was missing out on by not using Font Awesome as intended was that I couldn’t use their handy <code><i></code> tag shortcuts for specifying the icons to use.</p>
<p>Now, I have taken steps towards solving all of these many problems!</p>
<h2>Just use Font Awesome normally you weirdo</h2>
<p>Let’s back up a sec and talk about why I didn’t just use Font Awesome as intended in the first place (yes tldr; it is probably because I’m a weirdo).</p>
<p>Font Awesome has two ways that it can work: Web Fonts + <span class="caps">CSS</span>, or <span class="caps">SVG</span> + JavaScript. The former would involve retrieving an additional <span class="caps">CSS</span> file or two, as well as a couple of web fonts. The web font for the solid collection alone is 79.<span class="caps">4KB</span> - larger than anything else on this website. The JavaScript that would be required for the other method would likely be approaching <span class="caps">1MB</span> in size - larger than this <em>entire</em> website so far! I want a lean, fast-loading, low-power website, and these approaches seem entirely at odds with those goals.</p>
<p>It also struck me as odd to be statically generating a site, yet also having the client browser swapping in <span class="caps">SVG</span> images. I’ve nothing against JavaScript, but clearly this is work that can be done in advance!</p>
<h2>Doesn’t caching solve this problem?</h2>
<p>Well… maybe? In same cases? But not necessarily.</p>
<p>The average size of an icon in Font Awesome’s “solid” collection is 660B. A visitor would have to encounter over 1500 such embedded icons before downloading the JavaScript and caching it would be cheaper. The Web Fonts are much better, with caching the separate files becoming worthwhile after only 214 icons. That’s about 5 views of this blog’s index page, or 15 individual posts.</p>
<p>As such, if somebody reads 16 posts on this blog, they will have transferred more data than they would have if I’d used the Font Awesome web fonts. However, if 15 people read one post each and never visit again, the embedded approach comes out way ahead. So it very much depends on the traffic profile of the site, and I don’t think this site is one that people will be checking in on daily.</p>
<p>Embedding also offers other advantages, such as reducing initial load times.</p>
<h2>Solutions</h2>
<p>My solution is a <a href="https://github.com/khoulihan/pelican-embed-svg">pelican plugin</a> that post-processes the generated <span class="caps">HTML</span> files and embeds any SVGs it finds, whether specified as <code><img></code> tags or <code><i></code> tags.</p>
<p>It also, crucially, sets the <code>fill</code> attribute of any <span class="caps">SVG</span> paths to <code>currentColor</code>, which causes the fill colour to be taken from the current <span class="caps">CSS</span> text colour.</p>
<p>Taking the plugin beyond being merely a static implementation of Font Awesome, it also supports embedding of arbitrary <span class="caps">SVG</span> files. This can be achieved either by using <code><i></code> tags with the class <code>pi</code> to search a custom icon set, or through <code><img></code> tags where the <span class="caps">SVG</span> file is referenced by <span class="caps">URL</span>.</p>
<h2>Future</h2>
<p>The plugin probably has loads of rough edges at the moment. I haven’t at all tested if it supports Font Awesome’s more advanced behaviour, or even investigated how those features work, so there is a lot to be done there.</p>
<p>I may explore an approach that would combine the advantages of static generation with the advantages of a separate, cacheable <span class="caps">SVG</span> file. My initial thoughts on how to approach this plugin were to combine any referenced SVGs into a single file, and then reference them in the <span class="caps">HTML</span> using an <span class="caps">SVG</span> <code><use></code> tag. I need to learn a lot more about SVGs to know if that’s even feasible.</p>
<p>I also want to try to support other icon frameworks that support a similar <code><i></code> tag shortcut, such as <a href="https://forkaweso.me/Fork-Awesome/">Fork Awesome</a> and <a href="https://friconix.com">Friconix</a>.</p>
<p>In the meantime, it’s serving my purposes already on this site.</p>
<p><i class="fas fa-thumbs-up body-icon"></i> <i class="fas fa-bomb body-icon"></i> <i class="fas fa-cat body-icon"></i> <i class="fas fa-leaf body-icon"></i> <i class="fas fa-file body-icon"></i> <i class="fas fa-fist-raised body-icon"></i></p>Runtime Class Modification2020-03-25T17:51:00+01:002020-03-25T23:12:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-03-25:/runtime-class-modification.html<p>Batteries-included package development in Python requires an unorthodox approach when targeting microcontrollers.</p><p>Python is probably my favourite language, so I was excited some years ago when a project appeared on Kickstarter to develop a <a href="http://micropython.org/" title="MicroPython">Python runtime for microcontrollers</a>, and an associated microcontroller board.</p>
<p>However, writing Python for a microcontroller does have some constraints that aren’t really a factor when writing Python for other environments. Having maybe only <span class="caps">100KB</span> of <span class="caps">RAM</span> to work with, keeping code size as low as possible is essential.</p>
<p>When I wrote a <a href="https://github.com/khoulihan/micropython-tmp102" title="micropython-tmp102 repository">package to support the <span class="caps">TI</span> tmp102 temperature sensor</a>, I initially included all the required functionality in a single importable class. It used <span class="caps">15KB</span> of <span class="caps">RAM</span> after import, which does leave space for other code, but since some of the functionality is mutually exclusive I knew I could probably do better.</p>
<p>This post is about what I ended up with and how it works.</p>
<h2>Importable Features</h2>
<p>The core functionality of the package can be leveraged by importing the <code>Tmp102</code> class and creating an instance. This leaves the sensor in its default configuration, in which it performs a reading 4 times per second and makes the most recent available to your code on request. The details of initialising the object are explained in the <a href="https://github.com/khoulihan/micropython-tmp102/blob/master/README.md">documentation</a> if you actually want to use the module, so I won’t go into them again here.</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">machine</span> <span class="kn">import</span> <span class="n">I2C</span>
<span class="kn">from</span> <span class="nn">tmp102</span> <span class="kn">import</span> <span class="n">Tmp102</span>
<span class="n">bus</span> <span class="o">=</span> <span class="n">I2C</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">sensor</span> <span class="o">=</span> <span class="n">Tmp102</span><span class="p">(</span><span class="n">bus</span><span class="p">,</span> <span class="mh">0x48</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">sensor</span><span class="o">.</span><span class="n">temperature</span><span class="p">)</span>
</code></pre></div>
<p>That’s all well and good, but what if you want to make use of some of the more advanced features of the sensor, such as controlling the rate at which it takes readings (the “conversion rate”)? Such features are structured as importable modules which add the required functionality into the <code>Tmp102</code> class. The <code>CONVERSION_RATE_1HZ</code> constant in the example below, as well as other relevant code, are added to the class when the <code>conversionrate</code> module is imported.</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">tmp102</span> <span class="kn">import</span> <span class="n">Tmp102</span>
<span class="kn">import</span> <span class="nn">tmp102.conversionrate</span>
<span class="n">sensor</span> <span class="o">=</span> <span class="n">Tmp102</span><span class="p">(</span>
<span class="n">bus</span><span class="p">,</span>
<span class="mh">0x48</span><span class="p">,</span>
<span class="n">conversion_rate</span><span class="o">=</span><span class="n">Tmp102</span><span class="o">.</span><span class="n">CONVERSION_RATE_1HZ</span>
<span class="p">)</span>
</code></pre></div>
<p>If you don’t need to change the conversion rate in your project then the code to do so is never loaded. If you do need this or other features, all the functionality is still exposed through a single easy to use class.</p>
<h2>How?</h2>
<p>The package is structured like this:</p>
<div class="highlight"><pre><span></span><code>tmp102
+-- __init__.py
+-- _tmp102.py
+-- alert.py
+-- conversionrate.py
+-- convertors.py
+-- extendedmode.py
+-- oneshot.py
+-- shutdown.py
</code></pre></div>
<p>The base <code>Tmp102</code> class is defined in <code>_tmp102.py</code>, along with some private functions and constants.</p>
<div class="highlight"><pre><span></span><code><span class="n">REGISTER_TEMP</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">REGISTER_CONFIG</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">EXTENDED_MODE_BIT</span> <span class="o">=</span> <span class="mh">0x10</span>
<span class="k">def</span> <span class="nf">_set_bit</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">mask</span><span class="p">):</span>
<span class="k">return</span> <span class="n">b</span> <span class="o">|</span> <span class="n">mask</span>
<span class="k">def</span> <span class="nf">_clear_bit</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">mask</span><span class="p">):</span>
<span class="k">return</span> <span class="n">b</span> <span class="o">&</span> <span class="o">~</span><span class="n">mask</span>
<span class="k">def</span> <span class="nf">_set_bit_for_boolean</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">mask</span><span class="p">,</span> <span class="n">val</span><span class="p">):</span>
<span class="k">if</span> <span class="n">val</span><span class="p">:</span>
<span class="k">return</span> <span class="n">_set_bit</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">mask</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">_clear_bit</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">mask</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Tmp102</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">bus</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">temperature_convertor</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">bus</span> <span class="o">=</span> <span class="n">bus</span>
<span class="bp">self</span><span class="o">.</span><span class="n">address</span> <span class="o">=</span> <span class="n">address</span>
<span class="bp">self</span><span class="o">.</span><span class="n">temperature_convertor</span> <span class="o">=</span> <span class="n">temperature_convertor</span>
<span class="c1"># The register defaults to the temperature.</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_last_write_register</span> <span class="o">=</span> <span class="n">REGISTER_TEMP</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_extended_mode</span> <span class="o">=</span> <span class="kc">False</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
</code></pre></div>
<p>To hide the private stuff from users of the package, the <code>__init__.py</code> imports the <code>Tmp102</code> class and then removes the <code>_tmp102</code> module from the namespace.</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">tmp102._tmp102</span> <span class="kn">import</span> <span class="n">Tmp102</span>
<span class="k">del</span> <span class="n">_tmp102</span>
</code></pre></div>
<p>The interesting stuff happens in the feature sub-modules. Each feature module defines an <code>_extend_class</code> function which modifies the <code>Tmp102</code> class. Since importing a module runs it, this function can be called and then deleted to keep the namespace nice and clean - the module will actually be empty once imported. This pattern should be familiar to JavaScript developers!</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">_extend_class</span><span class="p">():</span>
<span class="c1"># Modify Tmp102 here - Check the next code block!</span>
<span class="k">pass</span>
<span class="n">_extend_class</span><span class="p">()</span>
<span class="k">del</span> <span class="n">_extend_class</span>
</code></pre></div>
<p>Let’s take a look at the <code>oneshot</code> module, which adds functionality to the <code>Tmp102</code> class to allow the sensor to be polled as necessary instead of constantly performing readings - very useful if you want to save power.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">_extend_class</span><span class="p">():</span>
<span class="kn">from</span> <span class="nn">tmp102._tmp102</span> <span class="kn">import</span> <span class="n">Tmp102</span>
<span class="kn">from</span> <span class="nn">tmp102._tmp102</span> <span class="kn">import</span> <span class="n">_set_bit_for_boolean</span>
<span class="kn">import</span> <span class="nn">tmp102.shutdown</span>
<span class="n">SHUTDOWN_BIT</span> <span class="o">=</span> <span class="mh">0x01</span>
<span class="n">ONE_SHOT_BIT</span> <span class="o">=</span> <span class="mh">0x80</span>
<span class="k">def</span> <span class="nf">initiate_conversion</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""</span>
<span class="sd"> Initiate a one-shot conversion.</span>
<span class="sd"> """</span>
<span class="n">current_config</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">current_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&</span> <span class="n">SHUTDOWN_BIT</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Device must be shut down to initiate one-shot conversion"</span><span class="p">)</span>
<span class="n">new_config</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">(</span><span class="n">current_config</span><span class="p">)</span>
<span class="n">new_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">_set_bit_for_boolean</span><span class="p">(</span>
<span class="n">new_config</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="n">ONE_SHOT_BIT</span><span class="p">,</span>
<span class="kc">True</span>
<span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_set_config</span><span class="p">(</span><span class="n">new_config</span><span class="p">)</span>
<span class="n">Tmp102</span><span class="o">.</span><span class="n">initiate_conversion</span> <span class="o">=</span> <span class="n">initiate_conversion</span>
<span class="k">def</span> <span class="nf">_conversion_ready</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">current_config</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">()</span>
<span class="k">return</span> <span class="p">(</span><span class="n">current_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&</span> <span class="n">ONE_SHOT_BIT</span><span class="p">)</span> <span class="o">==</span> <span class="n">ONE_SHOT_BIT</span>
<span class="n">Tmp102</span><span class="o">.</span><span class="n">conversion_ready</span> <span class="o">=</span> <span class="nb">property</span><span class="p">(</span><span class="n">_conversion_ready</span><span class="p">)</span>
</code></pre></div>
<p>So what’s going on here? First, the <code>Tmp102</code> class and any required functions are imported. Since it was imported in the package’s <code>__init__</code> the class is already defined. Importing the private functions and constants in a function like this keeps them out of the global namespace.</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">tmp102._tmp102</span> <span class="kn">import</span> <span class="n">Tmp102</span>
<span class="kn">from</span> <span class="nn">tmp102._tmp102</span> <span class="kn">import</span> <span class="n">_set_bit_for_boolean</span>
</code></pre></div>
<p>The <code>oneshot</code> module depends on the functionality from the <code>shutdown</code> module, so it is imported next.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">tmp102.shutdown</span>
</code></pre></div>
<p>Next, a couple of constants are defined. Through the magic of closure, these will only be available to the methods defined in this module.</p>
<div class="highlight"><pre><span></span><code><span class="n">SHUTDOWN_BIT</span> <span class="o">=</span> <span class="mh">0x01</span>
<span class="n">ONE_SHOT_BIT</span> <span class="o">=</span> <span class="mh">0x80</span>
</code></pre></div>
<p>The rest of the function defines a method and a property which are added to the class by simply assigning them to attributes. These will be available to any instances of the class, exactly as if they were included in the class definition.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">initiate_conversion</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""</span>
<span class="sd"> Initiate a one-shot conversion.</span>
<span class="sd"> """</span>
<span class="n">current_config</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">current_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&</span> <span class="n">SHUTDOWN_BIT</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Device must be shut down to initiate one-shot conversion"</span><span class="p">)</span>
<span class="n">new_config</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">(</span><span class="n">current_config</span><span class="p">)</span>
<span class="n">new_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">_set_bit_for_boolean</span><span class="p">(</span>
<span class="n">new_config</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="n">ONE_SHOT_BIT</span><span class="p">,</span>
<span class="kc">True</span>
<span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_set_config</span><span class="p">(</span><span class="n">new_config</span><span class="p">)</span>
<span class="n">Tmp102</span><span class="o">.</span><span class="n">initiate_conversion</span> <span class="o">=</span> <span class="n">initiate_conversion</span>
<span class="k">def</span> <span class="nf">_conversion_ready</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">current_config</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">()</span>
<span class="k">return</span> <span class="p">(</span><span class="n">current_config</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&</span> <span class="n">ONE_SHOT_BIT</span><span class="p">)</span> <span class="o">==</span> <span class="n">ONE_SHOT_BIT</span>
<span class="n">Tmp102</span><span class="o">.</span><span class="n">conversion_ready</span> <span class="o">=</span> <span class="nb">property</span><span class="p">(</span><span class="n">_conversion_ready</span><span class="p">)</span>
</code></pre></div>
<p>The other feature modules follow the same pattern.</p>
<h2>Savings</h2>
<p>Importing the base <code>Tmp102</code> class uses about 3.<span class="caps">53KB</span> of <span class="caps">RAM</span> - quite a saving if that is all you need. The feature modules vary between 0.<span class="caps">8KB</span> and <span class="caps">4KB</span>, or thereabouts. Importing them all uses 13.<span class="caps">44KB</span>, but it is unlikely that they would all be required in any given application.</p>
<h2>Conclusion</h2>
<p>I thought of this approach as “monkey-patching” for a long time - the last refuge of the desperate and the damned - but I’m not sure that it is really, because the modifications are all being made internally to the package. It is definitely outside the norm for Python, but it achieved the goal of reducing <span class="caps">RAM</span> usage while maintaining a clean <span class="caps">API</span>.</p>Self-Fulfilling Prophecies2020-03-22T15:43:00+01:002020-03-22T15:43:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-03-22:/prophecies.html<p>Expectations of shortages lead to shortages.</p><p><a href="https://pixabay.com/photos/toilet-paper-hamster-purchases-panic-4941768/"><img alt="Don't Panic" src="https://blog.hyperlinkyourheart.com/images/prophecies/toilet-paper.jpg"></a></p>
<p>We see it in every crisis - somebody posts a picture on social media of a bare shelf or a rumour goes around that the shops are running out of something (such as, to pick a good completely at random, toilet paper), and suddenly the shelves are emptying everywhere, and it seems to make sense to secure a stockpile.</p>
<p>It starts as an irrational fear, but it is reified by the seemingly rational self interests of individual consumers. It makes sense, on an individual level, to buy extra because everybody else is, or might be. The expectation of shortages leads to shortages, just as the expectation of economic growth helps create growth, and the fear of a crash leads to or worsens a crash, as everybody tries to get off the merry-go-round at the same time.</p>
<p>Market economies amplify and feed off our emotions and impulses in the face of incomplete information. We’re not generally privy to the details of the stocks and supply chains of any given good. If we were, we could determine whether a perceived shortage is real and how long it might be expected to last, and act accordingly. Even better than obtaining and acting on such information individually - which could still lead to panic buying in the event of an actual shortage - would be to evaluate and respond to the situation collectively, to ensure that everybody can get a reasonable share of goods even in the event of a shortage.</p>
<p>Markets don’t offer any mechanism for collective reasoning or action. The best a market can offer is price-gouging, where massive price increases disuade all but the most desparate until everybody comes to their senses. Thankfully, retailers in societies that haven’t completely devolved into neoliberal hellscapes tend to opt for rationing instead. Nobody wants to be seen to be a profiteer by a community that they are going to want to continue to serve after the crisis has passed.</p>
<p>It’s unfortunate that we have to be reliant on the reputational concerns of retailers to ensure the provision of essential goods in a crisis. The expectation of shortages leads to shortages, but somehow the certainty of occasional crises doesn’t lead to distributed production, resilient supply chains, or emergency stockpiles. Our economy’s blinkered focus on short-term profits and fetishisation of “efficiency” doesn’t allow for this kind of thinking.</p>Catnip2020-01-23T15:52:00+01:002020-01-23T16:24:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-01-23:/catnip.html<p>An animation of a cat hunting pixels on a 70’s styled computer.</p><p>I put together an animation over the last few weeks of a cat trying to catch <a href="https://en.wikipedia.org/wiki/Glider_(Conway%27s_Life)">gliders</a> on a 70’s styled computer. Check it out on YouTube:</p>
<p><a href="https://www.youtube.com/watch?v=2PqFIOkhUSk"><img alt="Catnip" src="https://img.youtube.com/vi/2PqFIOkhUSk/0.jpg"></a></p>
<p>I actually only set out to draw the computer, and I’m not sure at what point the cat entered the picture, but I’m glad it did! My goal wasn’t to depict any specific vintage computer, but to create a somewhat implausible one from imagination. I did look at a bunch of references for ideas on what to include, mostly from <a href="http://oldcomputers.net/">oldcomputers.net</a></p>
<h2>Music</h2>
<p>I had originally planned to create a patch in VCVRack to accompany the animation, but I struggled to create something that felt right for the animation. Several attempts to create something in <span class="caps">LMMS</span> also failed. I ended up putting something together in <a href="https://beepbox.co/#8n51s6k0l00e0zt2mm0a7g0zj07i0r1o321440T1v2L4u25q1d5f7y0z8C0c0A5F4B0V1Q000dPc696E0018T1v1L4u63q1d5f7y1z7C1c0A1F1B4V1Q50b0Pea3bE0181T1v3L4u57q1d5f4y4z2C1c0A0F9B4V8Q0040P9900E0111T1v3L4ue1q3d7f7y2zbC0c0A0F0B7V1Q0000Pe600E0911T0v1L4ua7q3d6f8y4z1C0w0c1h0T3v1L4uf7q1d5f7y3z6C1SZIztrsrzrqiiiiibhkki4N8l0000018j4xkg4S00000g4x0j4xci4N0i41ci4Q00hkki4N8j4xci4N8j4xd5hm00000000004x8i4x8i4x800000000000000000018j4xci4M00000h4x4i000h4x4i4h800000p23GKrF-9isT7URQVH-p4U7CpKkVJvGBbXHKtSRnrARlLmrnZFdv_jvonQYPIyYLLKKYXZDvBCR-9jnUtc3onQYMcU_0rrupCh1KrhZGxqJQmVKfZ-PLMFKf-jTY3V2CnQUaD8S0sxdvVOILbOWrnUbYDcdvonVCmq_se_aKU_W-U_wuIXz_HW_TW_iL0arF-EvcBC8bwkRUwnFB0zhjhy9BV9EZELOjhOs25E5PYAmwk0">beepbox</a>.</p>Bad Idioms2020-01-10T14:11:00+01:002020-03-25T23:20:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-01-10:/bad-idioms.html<p>A brief investigation of Python’s <span class="caps">EAFP</span> idiom applied to C#.</p><p>Human languages are full to the brim with idioms - figurative ways of saying things that native speakers trot out without even thinking about them. Often, when translated literally into another language, the result is utter nonsense. For example, the phrase “tomar el pelo” in Spanish translates literally to English as “to take the hair”, but the idiomatic way to say the same thing in English would be “to pull (someone’s) leg”. The same thing is roughly true of programming languages, with different languages having their own idiomatic or expected ways of achieving the same ends.</p>
<p>I recently made the mistake, after a period of writing Python code, of applying one of Python’s idioms to C#. The task at hand was to check if a dictionary of lists already contained a particular key, and if not, add a new list for that key. The C# way to do this would probably be to check for the existence of the key first, then decide what to do - or even better, use the <code>TryGetValue</code> method of the dictionary to assign the value to a variable. This is known as “Look Before You Leap”.</p>
<div class="highlight"><pre><span></span><code><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="w"> </span><span class="n">l</span><span class="p">;</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">dict</span><span class="p">.</span><span class="n">ContainsKey</span><span class="p">(</span><span class="n">objectType</span><span class="p">))</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">dict</span><span class="p">[</span><span class="n">objectType</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">objectType</span><span class="p">,</span><span class="w"> </span><span class="n">l</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<div class="highlight"><pre><span></span><code><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="w"> </span><span class="n">l</span><span class="p">;</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">dict</span><span class="p">.</span><span class="n">TryGetValue</span><span class="p">(</span><span class="n">objectType</span><span class="p">,</span><span class="w"> </span><span class="k">out</span><span class="w"> </span><span class="n">l</span><span class="p">))</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">objectType</span><span class="p">,</span><span class="w"> </span><span class="n">l</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>But instead of doing either of those things, I applied a more pythonic idiom - that of “Easier to Ask Permission than Forgiveness” - and just tried retrieving the value, and catching the <code>KeyNotFoundException</code>:</p>
<div class="highlight"><pre><span></span><code><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="w"> </span><span class="n">l</span><span class="p">;</span>
<span class="k">try</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">dict</span><span class="p">[</span><span class="n">objectType</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">KeyNotFoundException</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">object</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">objectType</span><span class="p">,</span><span class="w"> </span><span class="n">l</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>This turned an operation that should have taken milliseconds into one that was taking seconds, introducing a perceptible delay into my application.</p>
<p>Curious to know to exactly what extent performance differed between the above choices, and whether <span class="caps">EAFP</span> really would have been the better choice in Python, I decided to throw together some benchmark tests.</p>
<h2>Python</h2>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">timeit</span>
<span class="n">setup</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="s2">d = {</span>
<span class="s2"> 'a': [1, 2, 3,],</span>
<span class="s2"> 'b': [4, 5, 6,],</span>
<span class="s2"> 'c': [7, 8, 9,],</span>
<span class="s2">}</span>
<span class="s2">"""</span>
<span class="n">test_except</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="s2">try:</span>
<span class="s2"> v = d['d']</span>
<span class="s2">except KeyError:</span>
<span class="s2"> v = []</span>
<span class="s2"> d['d'] = v</span>
<span class="s2">del d['d']</span>
<span class="s2">"""</span>
<span class="n">test_check</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="s2">if 'd' in d:</span>
<span class="s2"> v = d['d']</span>
<span class="s2">else:</span>
<span class="s2"> v = []</span>
<span class="s2"> d['d'] = v</span>
<span class="s2">del d['d']</span>
<span class="s2">"""</span>
<span class="nb">print</span><span class="p">(</span><span class="n">timeit</span><span class="o">.</span><span class="n">timeit</span><span class="p">(</span><span class="n">setup</span><span class="o">=</span><span class="n">setup</span><span class="p">,</span> <span class="n">stmt</span><span class="o">=</span><span class="n">test_except</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1000000</span><span class="p">))</span>
<span class="nb">print</span><span class="p">(</span><span class="n">timeit</span><span class="o">.</span><span class="n">timeit</span><span class="p">(</span><span class="n">setup</span><span class="o">=</span><span class="n">setup</span><span class="p">,</span> <span class="n">stmt</span><span class="o">=</span><span class="n">test_check</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1000000</span><span class="p">))</span>
</code></pre></div>
<p>This gave results of 0.46 seconds for a million <span class="caps">EAFP</span> operations, and about 0.08 seconds for a million <span class="caps">LBYL</span> operations, with everything else, I hope, being equal between the two tests. If the new key is not deleted every time (so that only the first check fails), the <span class="caps">EAFP</span> operation becomes marginally faster than the alternative (0.026 vs 0.037 seconds) on most runs.</p>
<h2>C#</h2>
<div class="highlight"><pre><span></span><code><span class="n">Dictionary</span><span class="o"><</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">>></span><span class="w"> </span><span class="n">dict</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Dictionary</span><span class="o"><</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">>></span><span class="p">()</span>
<span class="p">{</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s">"a"</span><span class="p">,</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">()</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s">"b"</span><span class="p">,</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">()</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s">"c"</span><span class="p">,</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">()</span><span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
<span class="n">DateTime</span><span class="w"> </span><span class="n">exceptStart</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">;</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="m">1000</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="w"> </span><span class="n">v</span><span class="p">;</span>
<span class="w"> </span><span class="k">try</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">dict</span><span class="p">[</span><span class="s">"d"</span><span class="p">];</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">KeyNotFoundException</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">"d"</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Remove</span><span class="p">(</span><span class="s">"d"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">TimeSpan</span><span class="w"> </span><span class="n">exceptResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">exceptStart</span><span class="p">;</span>
<span class="n">DateTime</span><span class="w"> </span><span class="n">tryGetStart</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">;</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="m">1000000</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="w"> </span><span class="n">v</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">dict</span><span class="p">.</span><span class="n">TryGetValue</span><span class="p">(</span><span class="s">"d"</span><span class="p">,</span><span class="w"> </span><span class="k">out</span><span class="w"> </span><span class="n">v</span><span class="p">))</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">"d"</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Remove</span><span class="p">(</span><span class="s">"d"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">TimeSpan</span><span class="w"> </span><span class="n">tryGetResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">tryGetStart</span><span class="p">;</span>
<span class="n">DateTime</span><span class="w"> </span><span class="n">checkStart</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">;</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="m">1000000</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span>
<span class="p">{</span>
<span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="w"> </span><span class="n">v</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">dict</span><span class="p">.</span><span class="n">ContainsKey</span><span class="p">(</span><span class="s">"d"</span><span class="p">))</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="kt">string</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">"d"</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">else</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">dict</span><span class="p">[</span><span class="s">"d"</span><span class="p">];</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="n">dict</span><span class="p">.</span><span class="n">Remove</span><span class="p">(</span><span class="s">"d"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">TimeSpan</span><span class="w"> </span><span class="n">checkResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">checkStart</span><span class="p">;</span>
<span class="n">Console</span><span class="p">.</span><span class="n">WriteLine</span><span class="p">(</span><span class="s">"Except: {0}"</span><span class="p">,</span><span class="w"> </span><span class="n">exceptResult</span><span class="p">.</span><span class="n">TotalSeconds</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="n">WriteLine</span><span class="p">(</span><span class="s">"TryGet: {0:f10}"</span><span class="p">,</span><span class="w"> </span><span class="n">tryGetResult</span><span class="p">.</span><span class="n">TotalSeconds</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="n">WriteLine</span><span class="p">(</span><span class="s">"Check: {0:f10}"</span><span class="p">,</span><span class="w"> </span><span class="n">checkResult</span><span class="p">.</span><span class="n">TotalSeconds</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="n">ReadKey</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
</code></pre></div>
<p>Note that the <span class="caps">EAFP</span> test here is only performed a thousand times - because even running it that many times takes around 15 <em>entire</em> seconds! The two <span class="caps">LBYL</span> tests are nothing in comparison, executing a million times in around 0.05 seconds. This is a much bigger difference than I would have expected.</p>
<h2>Conclusion</h2>
<p>The performance of a single operation like this doesn’t necessarily say a lot about the real-world performance of any given application, but I think it is probably best to stick to the idioms of the language you’re working in - and in C#, that means only throwing exceptions in exceptional circumstances. In Python, there may be circumstances where it would be better to “Look Before You Leap” as well, but the difference in performance is probably not large enough to matter in most cases.</p>Remember Blogs?2020-01-02T20:05:00+01:002020-01-02T20:05:00+01:00Kevin Houlihantag:blog.hyperlinkyourheart.com,2020-01-02:/remember-blogs.html<p>The how and the why of this very blog right here.</p><p>I’ve read a lot of articles recently (<a href="https://omarabid.com/the-modern-web">here’s one</a>) lamenting the state of the web. Once distributed, egalitarian, ungovernable, and fast, now centralised, intentionally manipulative, and bloated both technically and conceptually. Even when you manage to fight your way through the popups demanding your attention or personal information, often what is underneath is not worth the effort - more likely a vehicle for advertising than for insight.</p>
<p>It’s also incredibly power-hungry. It’s hard to tie down an exact figure for exactly <em>how</em> power-hungry, but the internet as a whole <a href="https://newrepublic.com/article/155993/can-internet-survive-climate-change">could account for up to 10% of global energy use</a>. A good chunk of that is streaming video and music, which is a topic for another day, but of the power consumed in serving the web, some of it is related to actual valuable content that people want to see, and some of it is related to the trends described above. The latter is waste. At least bloated JavaScript and <span class="caps">CSS</span> frameworks can be cached, but advertising has to be constantly served anew.</p>
<p>So, anyway, all this to say… I’ve decided to start a blog.</p>
<h2>The Tech</h2>
<p>My technical goals for this website are for it to be…</p>
<ul>
<li><strong>Lightweight <span class="amp">&</span> fast to load</strong> - I set up a WordPress site recently, on the best hosting I can afford. It is not lightweight or fast to load.</li>
<li><strong>Content focused</strong> - Read one thing or read them all, but I’m sure you can only read one article at a time.</li>
<li><strong>Nice to look at</strong> - Apparently it <a href="https://perfectmotherfuckingwebsite.com/">doesn’t take much</a>. Also going for consistent <em>branding</em> between all my sites and profiles.</li>
<li><strong>Responsive</strong> - Readable on phones as well as desktops!</li>
<li><strong>Easy to deploy</strong> - I don’t have time to configure and maintain a teetering stack of back-end technology, and if I have to move to different hosting at some point, I want it to be a simple task.</li>
<li><strong>Easy to update</strong> - If writing posts is a chore, I won’t ever do it.</li>
<li><strong>Hackable</strong> - Created using technologies that I’m somewhat familiar with, so that it is feasible for me to modify or extend if I want/need to.</li>
</ul>
<p>I decided almost immediately that a statically-generated site was going to be the best way to achieve most of those goals. I’m a big fan of Python, so although <em>hackability</em> could be achieved by a JavaScript or C# based generator, I checked out the Python ones first, and found plenty of viable options. I settled on <a href="https://blog.getpelican.com/">Pelican</a> because it’s…</p>
<ul>
<li><strong>Popular</strong> - It seems to be one of the more popular Python generators.</li>
<li><strong>Blog-oriented</strong> - Some generators are geared towards documentation or are intended as replacements for content management systems, but that’s not what I’m doing.</li>
<li><strong>Supports Markdown</strong> - I’m sure reST is fine, but I already have to use Markdown elsewhere so I’d rather stick with that.</li>
<li><strong>Easy to update</strong> - Just create a new Markdown file and run a command to rebuild.</li>
<li><strong>Extensible</strong> - It includes a plugin system to modify the output.</li>
</ul>
<p>I also decided to hand-craft my own theme, and to avoid a <span class="caps">CSS</span> framework. I love the look of Bootstrap, and how quick it is to get started with, but it’s over 200kb and a lot of that is undoubtedly unnecessary for my needs. The spirit of the exercise is bare-bones and <span class="caps">DIY</span>!</p>
<h3>The Theme</h3>
<p>The first step in hand-crafting a theme was… to find an existing theme to copy! <a href="https://github.com/arulrajnet/attila">Atilla</a> was the closest to the style I was after, so I took a copy of that and gutted it of <span class="caps">CSS</span> and JavaScript and other elements that didn’t meet my needs. Then I started building the <span class="caps">CSS</span> back up while trying to keep it as minimal as possible. It may not implement every feature supported by Pelican, but you can find it <a href="https://github.com/khoulihan/hyh-blog/tree/master/themes/hyper">on my Github</a> if it seems like something you could adapt for your own needs.</p>
<p>One departure that I made from the standard Pelican configuration was to have the social media links be taken from a collection of tuples with three elements, so that I could specify both an icon and a title to use.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Custom social list that includes icons</span>
<span class="n">SOCIAL_ICONS</span> <span class="o">=</span> <span class="p">((</span><span class="s1">'Twitter'</span><span class="p">,</span> <span class="s1">'twitter.svg'</span><span class="p">,</span> <span class="s1">'https://twitter.com/http_your_heart'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'Mastodon'</span><span class="p">,</span> <span class="s1">'mastodon.svg'</span><span class="p">,</span> <span class="s1">'https://mastodon.art/@hyperlinkyourheart'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'Instagram'</span><span class="p">,</span> <span class="s1">'instagram.svg'</span><span class="p">,</span> <span class="s1">'https://www.instagram.com/hyperlinkyourheart/'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'YouTube'</span><span class="p">,</span> <span class="s1">'youtube.svg'</span><span class="p">,</span> <span class="s1">'https://www.youtube.com/channel/UCc_O9Hp5UfQ-IHswi1H54Zg'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'Twitch'</span><span class="p">,</span> <span class="s1">'twitch.svg'</span><span class="p">,</span> <span class="s1">'https://www.twitch.tv/hyperlinkyourheart'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'Itch'</span><span class="p">,</span> <span class="s1">'itchio.svg'</span><span class="p">,</span> <span class="s1">'https://hyperlinkyourheart.itch.io/'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'GitHub'</span><span class="p">,</span> <span class="s1">'github.svg'</span><span class="p">,</span> <span class="s1">'https://github.com/khoulihan'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'Atom Feed'</span><span class="p">,</span> <span class="s1">'rss.svg'</span><span class="p">,</span> <span class="s1">'/feeds/all.atom.xml'</span><span class="p">),)</span>
</code></pre></div>
<p>I like that I can just throw custom configuration into the config file and then make use of it in the templates. However, it probably makes the theme less generally useful.</p>
<p>As it stands currently, loading this post requires less than 30kb to be transferred.</p>
<h3>Plugins</h3>
<p>Currently, the only plugin I’m using is the <a href="https://github.com/getpelican/pelican-plugins/tree/master/css-html-js-minify">css-html-js-minify</a> plugin that is available in the pelican-plugins repository. I haven’t found anything I need to write my own plugin to handle yet, but I’m sure I will get to it.</p>
<p>One problem that needs solving is that the <span class="caps">SVG</span> icons are a big nuisance, because it doesn’t seem to be possible to change their colour without using the <span class="caps">CSS</span> <code>filter</code> property, which is not nearly as convenient as just setting the colour directly. In order to do that, using the <code>fill</code> property, I would have to embed the SVGs, or reference them as symbols in a <code><use></code> tag within an <code><svg></code> tag. The individual icon files (from <a href="https://fontawesome.com/">FontAwesome</a>) aren’t set up like that, and I didn’t want to use their spritesheet because it is rather large.</p>
<p>What I might do in the future is write a plugin to compile the individual files into a single spritesheet of symbols, then find and replace any references to them with appropriate <code><svg></code> tags. Essentially this will be doing the job that the FontAwesome toolkit usually does in the browser.</p>
<h2>The Content</h2>
<p>Uuuh… I’ll get back to you on that. Things I like, things I do, that sort of thing.</p>
<h2>Feedback</h2>
<p>There’s a couple of different strategies for allowing comments on a static site - I’m not going to attempt them for now, and perhaps never will! If you have any feedback or thoughts there are many ways to reach me, such as <a href="https://mastodon.art/@hyperlinkyourheart">Mastodon</a> or <a href="https://twitter.com/http_your_heart">Twitter</a>, and I think that’s just fine.</p>