<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.1">Jekyll</generator><link href="https://brianustas.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://brianustas.com/" rel="alternate" type="text/html" /><updated>2026-11-14T03:54:01+00:00</updated><id>https://brianustas.com/feed.xml</id><title type="html">Brian Ustas</title><subtitle>The personal website of Brian Ustas. View his projects and blog posts here.</subtitle><author><name>Brian Ustas</name></author><entry><title type="html">VividVocab</title><link href="https://brianustas.com/blog/vividvocab" rel="alternate" type="text/html" title="VividVocab" /><published>2026-01-01T00:00:00+00:00</published><updated>2026-11-14T01:36:28+00:00</updated><id>https://brianustas.com/blog/vividvocab</id><content type="html" xml:base="https://brianustas.com/blog/vividvocab"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>VividVocab</h1>
    <span class="post-date">Jan 1, 2026</span>
  </div>

  
    <ul class="links">
      <li><a href="https://apps.apple.com/app/id6575389517">App Store</a></li>
      <li><a href="https://play.google.com/store/apps/details?id=com.vividvocab.android">Google Play</a></li>
      <li><a href="https://www.vividvocab.com">website</a></li>
      </ul>
  

  
    <ul class="tags">
      <li>React Native</li>
      <li>Expo</li>
      <li>Next.js</li>
      <li>Ruby on Rails</li>
      <li>PostgreSQL</li>
      <li>AI</li>
      <li>app design</li>
      </ul>
  
</div><p>VividVocab is an English vocabulary builder and visual dictionary that I built. It brings new words to life with memorable images, clear explanations, and spaced repetition, so you actually remember them.</p><p>It works in three steps:</p><ol type="1"><li><strong>Capture:</strong> look up and save any word, phrase, or idea.</li><li><strong>Visualize:</strong> turn it into a mnemonic image.</li><li><strong>Remember:</strong> master it with spaced repetition and quizzes.</li></ol><p>Review your words for a few minutes a day and you’ll build an impressive vocabulary, the same way you’d build an impressive physique by going to the gym.</p><p>It’s for people learning English, students studying for the SAT or GRE, and anyone who wants to stop forgetting the words they look up.</p><p>Get it on the <a href="https://apps.apple.com/app/id6575389517">App Store</a> or <a href="https://play.google.com/store/apps/details?id=com.vividvocab.android">Google Play</a>, or read more at <a href="https://www.vividvocab.com">vividvocab.com</a>.</p><h2 id="screenshots">Screenshots</h2><div class="row"><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_649,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v247491228/brianustas_com/vividvocab/apricity-flashcard@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1298,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v247491228/brianustas_com/vividvocab/apricity-flashcard@2x.png 2x" width="649" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v247491228/brianustas_com/vividvocab/apricity-flashcard@2x.png" data-dimen="649x1126" /><figcaption>Turn any word into a mnemonic flashcard.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_649,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v287386294/brianustas_com/vividvocab/smart-search@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1298,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v287386294/brianustas_com/vividvocab/smart-search@2x.png 2x" width="649" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v287386294/brianustas_com/vividvocab/smart-search@2x.png" data-dimen="649x1126" /><figcaption>Look up flashcards for any word.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v346756793/brianustas_com/vividvocab/spaced-repetition@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v346756793/brianustas_com/vividvocab/spaced-repetition@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v346756793/brianustas_com/vividvocab/spaced-repetition@2x.png" data-dimen="646x1135" /><figcaption>Master words with spaced repetition.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v812970034/brianustas_com/vividvocab/quizzes@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v812970034/brianustas_com/vividvocab/quizzes@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v812970034/brianustas_com/vividvocab/quizzes@2x.png" data-dimen="646x1135" /><figcaption>Stay sharp with quick quizzes.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v949795351/brianustas_com/vividvocab/tell-me-more@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v949795351/brianustas_com/vividvocab/tell-me-more@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v949795351/brianustas_com/vividvocab/tell-me-more@2x.png" data-dimen="646x1135" /><figcaption>Every word, simply explained.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v902835624/brianustas_com/vividvocab/made-for-you@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v902835624/brianustas_com/vividvocab/made-for-you@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v902835624/brianustas_com/vividvocab/made-for-you@2x.png" data-dimen="646x1135" /><figcaption>Discover words curated for you.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v422970734/brianustas_com/vividvocab/my-decks@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v422970734/brianustas_com/vividvocab/my-decks@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v422970734/brianustas_com/vividvocab/my-decks@2x.png" data-dimen="646x1135" /><figcaption>Study decks for exams and life.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_646,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v395599890/brianustas_com/vividvocab/habit@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v395599890/brianustas_com/vividvocab/habit@2x.png 2x" width="646" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v395599890/brianustas_com/vividvocab/habit@2x.png" data-dimen="646x1135" /><figcaption>Build a habit you’ll stick with.</figcaption></figure></div></div>]]></content><author><name>Brian Ustas</name></author><category term="React Native" /><category term="Expo" /><category term="Next.js" /><category term="Ruby on Rails" /><category term="PostgreSQL" /><category term="AI" /><category term="app design" /><summary type="html"><![CDATA[English vocabulary builder and visual dictionary for iOS and Android. It turns words into memorable images, so you actually remember them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/vividvocab.png" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/vividvocab.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Conditional Github Actions via Git Commits</title><link href="https://brianustas.com/blog/conditional-github-actions" rel="alternate" type="text/html" title="Conditional Github Actions via Git Commits" /><published>2022-02-16T00:00:00+00:00</published><updated>2024-08-02T04:57:22+00:00</updated><id>https://brianustas.com/blog/conditional-github-actions</id><content type="html" xml:base="https://brianustas.com/blog/conditional-github-actions"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Conditional Github Actions via Git Commits</h1>
    <span class="post-date">Feb 16, 2022</span>
  </div>

  

  
    <ul class="tags">
      <li>tidbit</li>
      <li>Github Actions</li>
      </ul>
  
</div><p>When using Github Actions, Github <a href="https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs">supports skipping workflow runs</a> by adding <code>[no ci]</code> to your Git commits.</p><p>Conversely, to help reduce your Github bill, this post shares a way to make workflow jobs opt-in by adding <code>[ci]</code> to your Git commit.</p><h2 id="github-actions-can-get-expensive">Github Actions can get expensive</h2><p><a href="https://github.com/features/actions">Github Actions</a> are awesome and allow automating software workflows to streamline development. At <a href="https://hub3.com">Hub3</a>, we use Github Actions to automate the below jobs for our workflow named <code>ci</code> (short for <a href="https://en.wikipedia.org/wiki/Continuous_integration">continuous integration</a>):</p><ol type="1"><li><code>lint</code>: check the app’s code for consistent style and best practices<ul><li>e.g. <code>yarn lint</code></li></ul></li><li><code>test</code>: run automated tests to verify that the app’s working correctly<ul><li>e.g. <code>bin/rspec</code></li></ul></li><li><code>deploy</code>: push the app’s code to the live production server</li></ol><p>By default (when using <code>on: push</code>), the <code>lint</code> and <code>test</code> jobs are <em>automatically run for each new Git commit</em>. This is uber convenient and allows our developers to receive fast feedback about their code’s style and functionality. Here’s what that looks like at the bottom of a <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests">pull request</a>:</p><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_467,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v155515056/brianustas_com/conditional-github-ci/github-actions-checks@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_934,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v155515056/brianustas_com/conditional-github-ci/github-actions-checks@2x.png 2x" width="467" data-dimen="467x221" /><figcaption>All jobs are passing. 🙂</figcaption></figure><p>Sadly, Github Actions are not free for private repositories, and the cost is based on how long your jobs take.</p><p>As your project grows, your jobs will likely take longer due to more code to lint, more tests to run, etc. This trend compounds as you add developers to the project.</p><p>Eventually you might ask: <em>How can we use Github Actions more efficiently and reduce our Github bill? Make jobs opt-in instead?</em></p><h2 id="conditionally-running-github-actions">Conditionally running Github Actions</h2><p>Well, I asked that question!</p><p>When working on a large feature, it’s rare to want to run Github Actions for EVERY commit. For instance, some commits act as checkpoints, others as quick fixes, etc. In those cases, it’s often premature to run the <code>test</code> and <code>lint</code> jobs because the code is in a WIP state.</p><p><strong>To run Github Actions only when we want to, we add the <code>[ci]</code> flag to our Git commits.</strong> For example, a pull request’s WIP commits might look like this:</p><div class="no-wrap-code"><pre><code>* 2015073 - add payment migrations
* a778af1 - add payment controllers
* beecca1 - add payment model and controller specs [ci]
* 3c46eca - add simple payment frontend
* 216f3c1 - ui/ux improvements
* 78165cd - add feature specs [ci]
* a7a5f37 - address feedback
* e0fec97 - address more feedback
* 1b7442e - fix linting issues [ci]</code></pre></div><p>In that example, we only run our <code>ci</code> workflow 3 times instead of 9, saving 66% on Github Actions costs for that pull request.</p><p><strong>To add support for the <code>[ci]</code> flag, we added the <code>if:</code> statement under each of our workflow’s jobs:</strong></p><div class="no-wrap-code"><pre class="yaml"><code># .github/workflows/ci.yml

lint:
  if: &quot;contains(toJSON(github.event.commits.*.message), &#39;[ci]&#39;) || github.ref == &#39;refs/heads/main&#39;&quot;
  ...

test:
  if: &quot;contains(toJSON(github.event.commits.*.message), &#39;[ci]&#39;) || github.ref == &#39;refs/heads/main&#39;&quot;
  ...</code></pre></div><p>That’s it! Feel free to rename the <code>[ci]</code> flag to whatever you want. Also, notice that the jobs will always run if the branch is <code>main</code>, which is usually what you want.</p><p>Alternatively, you could introduce <code>[ci-test]</code> and <code>[ci-lint]</code> flags for more granular control over when jobs run. At Hub3, given the speed of our <code>lint</code> job and the overhead of two flags to remember, we didn’t.</p><p>If you find a simpler way to accomplish opt-in Github Actions, shoot me a note.</p>]]></content><author><name>Brian Ustas</name></author><category term="tidbit" /><category term="Github Actions" /><summary type="html"><![CDATA[Save money by only running Github Actions when you need to.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/brian-headshot-bermuda.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/brian-headshot-bermuda.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Reducing React Query Boilerplate</title><link href="https://brianustas.com/blog/reducing-react-query-boilerplate" rel="alternate" type="text/html" title="Reducing React Query Boilerplate" /><published>2022-01-22T00:00:00+00:00</published><updated>2026-11-14T03:37:56+00:00</updated><id>https://brianustas.com/blog/reducing-react-query-boilerplate</id><content type="html" xml:base="https://brianustas.com/blog/reducing-react-query-boilerplate"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Reducing React Query Boilerplate</h1>
    <span class="post-date">Jan 22, 2022</span>
  </div>

  

  
    <ul class="tags">
      <li>tidbit</li>
      <li>React Query</li>
      <li>async frontend state</li>
      </ul>
  
</div><p>At <a href="https://hub3.com">Hub3</a>, <a href="https://tanstack.com/query/latest">React Query</a> has been a game-changer for how we handle frontend state. This post describes a helpful React Query pattern we developed that reduces boilerplate and improves our productivity.</p><h2 id="wait-react-query">Wait, React Query?</h2><p>A quick primer for the unfamiliar: React Query describes itself as a “data-fetching library for React.” It encourages you to reflect on your frontend state’s “ownership” and separate it into <strong>client state</strong> and <strong>server state.</strong></p><ul><li><strong>Client state</strong> is created by the app’s frontend and doesn’t live anywhere else.<ul><li>e.g. <code>selectedBookIds</code> , <code>isModalOpen</code> (aka <strong>UI state</strong>)</li></ul></li><li><strong>Server state</strong> is the opposite—state that your client fetches asynchronously from some backend.<ul><li>e.g. <code>books</code> , <code>userProfile</code></li></ul></li></ul><p>React Query makes working with <strong>server state</strong> easier by providing hooks to help you fetch it, manage asynchronous loading/error states, and cache it via the <a href="https://web.dev/articles/stale-while-revalidate">stale-while-revalidate</a> pattern. At Hub3, React Query dramatically reduced our reliance on <a href="https://redux-toolkit.js.org">Redux Toolkit</a>, which removed <em>a lot</em> of Redux state management code and sped up development.</p><p>Check out React Query’s <a href="https://tanstack.com/query/latest/docs/framework/react/overview">overview and motivation</a> for the full scoop.</p><h2 id="the-boilerplate">The Boilerplate</h2><p>Consider the example below that uses React Query’s excellent <a href="https://tanstack.com/query/latest/docs/framework/react/reference/useQuery"><code>useQuery</code> hook</a> via custom hooks. Try to focus on how asynchronous loading and error states are handled (and not on the contrived page structure 🙃):</p><div class="no-wrap-code"><pre class="javascript"><code>const BookShowPage = props =&gt; {
  const {
    data: book,
    isLoading: isLoadingBook,
    error: bookLoadingError,
  } = useBookQuery(props.bookId);

  const {
    data: author,
    isLoading: isLoadingAuthor,
    error: authorLoadingError,
  } = useAuthorQuery(book.authorId, { isEnabled: !!book });

  const {
    data: similarBooks,
    isLoading: isLoadingSimilarBooks,
    error: similarBooksLoadingError,
  } = useSimilarBooksQuery(props.bookId);

  // Show a loader if necessary.
  if (isLoadingBook ||
      isLoadingAuthor ||
      isLoadingSimilarBooks) {
    return &lt;Loader /&gt;;
  }

  // Show an error message if necessary.
  const loadingError =
    bookLoadingError ||
    authorLoadingError ||
    similarBooksLoadingError;
  if (loadingError) {
    return &lt;Error error={loadingError} /&gt;;
  }

  return (
    &lt;PageContainer&gt;
      &lt;BookSummary book={book} /&gt;
      &lt;AuthorSummary author={author} /&gt;
      &lt;SimilarBooksCarousel books={similarBooks} /&gt;
    &lt;/PageContainer&gt;
  );
};</code></pre></div><p>Not bad! But we can do a bit better. Here are my two issues:</p><ol type="1"><li>For each query, you need two new variables to store the loading and error state (e.g. <code>isLoadingBook</code> and <code>bookLoadingError</code>). You also need to update the loading and error conditionals.<ul><li>This pattern becomes cumbersome with a few <code>useQuery</code> statements.</li></ul></li><li>For each component that fetches data asynchronously, you need to remember to show loading and error states.<ul><li>In my experience, the latter is often forgotten.</li></ul></li></ol><h2 id="the-boilerplate-killer">The Boilerplate Killer</h2><p>To address the pain points from above, we created <code>reactQueryRenderHelper</code> to simplify components with multiple asynchronous queries and standardize our loading and error handling pattern.</p><p>To understand how it works, let’s refactor the above example:</p><div class="no-wrap-code"><pre class="javascript"><code>const BookShowPage = props =&gt; {
  const { data: book, ...bookQueryAttrs } =
    useBookQuery(props.bookId);

  const { data: author, ...authorQueryAttrs } =
    useAuthorQuery(book.authorId, { isEnabled: !!book });

  const { data: similarBooks, ...similarBooksQueryAttrs } =
    useSimilarBooksQuery(props.bookId);

  const asyncStatusComponent = reactQueryRenderHelper([
    bookQueryAttrs,
    authorQueryAttrs,
    similarBooksQueryAttrs,
  ]);

  if (asyncStatusComponent) {
    return asyncStatusComponent;
  }

  return (
    &lt;PageContainer&gt;
      &lt;BookSummary book={book} /&gt;
      &lt;AuthorSummary author={author} /&gt;
      &lt;SimilarBooksCarousel books={similarBooks} /&gt;
    &lt;/PageContainer&gt;
  );
};</code></pre></div><p>Kinda neat!</p><p>What is <code>asyncStatusComponent</code>? In this case, it’s either <code>undefined</code>, <code>&lt;Loader /&gt;</code> or <code>&lt;Error /&gt;</code>. <code>reactQueryRenderHelper</code> decides which one based on its arguments. The component doesn’t know or care—it just renders <code>asyncStatusComponent</code> if it’s present and otherwise trusts that the query data is ready to use.</p><p>A few benefits to this pattern:</p><ul><li>⬇️ Boilerplate: Fewer variables and conditionals</li><li>⬆️ Standardization: Loading and error state is handled identically across components.</li><li>⬆️ Robustness: You can’t forget to handle and display asynchronous errors (which I see happen quite a bit).</li></ul><h2 id="reactqueryrenderhelper-implementation">reactQueryRenderHelper implementation</h2><p>Below is our implementation of <code>reactQueryRenderHelper</code>. To use it in your project, be sure to bring your own <code>&lt;Loader /&gt;</code> and <code>&lt;Error /&gt;</code> components:</p><div class="no-wrap-code"><pre class="javascript"><code>// Vendor
import _ from &quot;lodash&quot;;
// Components
import Loader from &quot;~/components/shared/loader&quot;;
import Error from &quot;~/components/shared/error&quot;;

// This helper inspects the results from one or more react-query useQuery calls.
// It decides what component should be rendered (Loader, Error, etc.) and
// returns it to the caller. This helps reduce tedious boilerplate code.
//
// opts:
// - loaderProps: props for shared/loader
// - errorProps: props for shared/error
// - loaderComponent: override loader component
// - errorComponent: override error component
const reactQueryRenderHelper = (reactQueryAttrs, opts = {}) =&gt; {
  const isLoading = _.some(reactQueryAttrs, { isLoading: true });
  if (isLoading) {
    return opts.loaderComponent || &lt;Loader {...opts.loaderProps} /&gt;;
  }

  const isError = _.some(reactQueryAttrs, { isError: true });
  if (isError) {
    return opts.errorComponent || &lt;Error {...opts.errorProps} /&gt;;
  }

  return undefined;
};

export default reactQueryRenderHelper;</code></pre></div>]]></content><author><name>Brian Ustas</name></author><category term="tidbit" /><category term="React Query" /><category term="async frontend state" /><summary type="html"><![CDATA[A pattern for managing multiple async queries concisely with React Query.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/brian-headshot-bermuda.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/brian-headshot-bermuda.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Creating Hub3’s logo with the Golden Ratio</title><link href="https://brianustas.com/blog/hub3-logo-golden-ratio" rel="alternate" type="text/html" title="Creating Hub3’s logo with the Golden Ratio" /><published>2021-06-13T00:00:00+00:00</published><updated>2026-11-14T01:21:46+00:00</updated><id>https://brianustas.com/blog/hub3-logo-golden-ratio</id><content type="html" xml:base="https://brianustas.com/blog/hub3-logo-golden-ratio"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Creating Hub3’s logo with the Golden Ratio</h1>
    <span class="post-date">Jun 13, 2021</span>
  </div>

  

  
    <ul class="tags">
      <li>Hub3</li>
      <li>logo</li>
      <li>Golden Ratio</li>
      <li>design constraints</li>
      <li>Sketch</li>
      </ul>
  
</div><div style="max-width: 20rem; margin: auto;"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v332754647/brianustas_com/hub3-logo/hub3_logo_and_text.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v332754647/brianustas_com/hub3-logo/hub3_logo_and_text.png 2x" width="700" data-dimen="800x326" /><figcaption><a href="https://hub3.com">Hub3</a>’s logo</figcaption></figure></div><h2 id="logo-goals">Logo Goals</h2><p>I wanted <a href="https://hub3.com">Hub3</a>’s logo to encapsulate what Hub3 stands for: a hub for strategy, design, and code. Those four concepts (including the hub) need their own distinct components in the logo.</p><p>The logo also needs to look great in various contexts: our website, LinkedIn, invoices, etc. After a quick survey of those contexts, I decided that our logo needs to scale well at small sizes and is perfectly centered when framed in a circle or square (think Facebook vs. LinkedIn profile images).</p><p>Lastly, I wanted the logo to be <a href="https://en.wikipedia.org/wiki/Vector_graphics">vector-based</a> (instead of pixel-based) to be resizable without quality loss. It should also be built with simple primitives (circles, rectangles, etc.) so that it’s easily reproducible. I picked <a href="https://www.sketch.com/">Sketch</a> as my design tool because I’m most familiar with it.</p><h2 id="problem-never-ending-iteration">Problem: Never-ending Iteration</h2><p>Before jumping into the logo’s construction, I’ll describe the design process I used and the pain points it addresses.</p><p>Given the subjectivity of design and all the options for colors and shapes, I find it hard to settle on any design confidently. There always seems to be a tweaked variation that’s incrementally better—a brighter blue, larger corner radius, etc. As a result, I’ve spent way too much time chasing incremental design improvements, feeling unsatisfied.</p><p>For example, here are some questions that cause me to spin my wheels:</p><ul><li>How big should this circle be?</li><li>What’s the perfect shade of red?</li><li>How much padding should be between a logo and its frame?</li><li>How curved should this edge be?</li></ul><p>To answer those questions, I want to trust in a process that doesn’t rely entirely on my gut or the opinions of others.</p><h2 id="solution-constraints">Solution: Constraints</h2><p>How do I design faster with more confidence? For me, it’s simplifying my design process and adding constraints that prevent me from chasing incremental gains.</p><p>Here are the constraints I came up with for the Hub3 logo project:</p><ul><li>Colors can only be picked from a popular color catalog (e.g. <a href="https://colors.lol/">colors.lol</a>).</li><li>The logo can only be built with primitive shapes: circles, triangles, and rectangles.</li><li>The Golden Ratio should be used to define the initial sizes of primitives.</li></ul><p>That’s my sandbox! Let’s dive into the last constraint, which this post focuses on.</p><h2 id="golden-ratio">Golden Ratio</h2><p><strong>For our purposes, the <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden Ratio</a> (or “divine proportion”) is a ratio between two lengths that produce proportions considered aesthetically pleasing.</strong> It appears in natural and man-made contexts like architecture and financial markets. Specifically, it’s an irrational number represented by <span class="math inline">\(\phi\)</span> and commonly truncated to <strong>1.618</strong>.</p><p>Here’s the Golden Ratio in action, for segments <span class="math inline">\(\overline{\rm AC}\)</span> and <span class="math inline">\(\overline{\rm CB}\)</span>:</p><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_292,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v656665052/brianustas_com/hub3-logo/golden_ratio.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_584,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v656665052/brianustas_com/hub3-logo/golden_ratio.png 2x" width="292" data-dimen="292x118" /><figcaption>So aesthetic! <a href="https://mathworld.wolfram.com/GoldenRatio.html">image credit</a></figcaption></figure><p>Interestingly, the Golden Ratio is hidden in the <a href="https://en.wikipedia.org/wiki/Fibonacci_number">Fibonacci Sequence</a>. Consider a segment of the Fibonacci Sequence below:</p><pre><code>0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...</code></pre><p>As you progress through the sequence and divide each number by the previous, you get closer and closer to the Golden Ratio. For example:</p><p><span class="math display">\[
2 \div 1 = 2 \\
8 \div 5 = 1.6 \\
34 \div 21 \approx 1.619... \\
144 \div 89 \approx ~1.618
\]</span></p><p>Cool! Now we know what the Golden Ratio is and how to approximate it. Let’s put it to use.</p><h2 id="logo-construction">Logo Construction</h2><p>Jumping ahead, I know we’ll need to start with a bunch of circles. How big should the circles be? Let’s use the Golden Ratio!</p><p>First, pick an initial circle size—I picked 300px. Create the next circle by multiplying the first circle’s diameter by the Golden Ratio, or 1.6 (you could use 1.618, but that results in more non-integer products). Repeat this three more times until you have five overlapping circles:</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v929968060/brianustas_com/hub3-logo/circle_primitives.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v929968060/brianustas_com/hub3-logo/circle_primitives.png 2x" width="700" data-dimen="800x800" /></p><p>Easy! The big idea is that we can now trust that these “divine proportions” will care over into the derived logo.</p><p>How should we represent the “hub” in Hub3? The <a href="https://www.merriam-webster.com/dictionary/hub">Webster dictionary</a> defines a hub as “a center of activity” or “the central part of a circular object.” I think a circle works, and I picked the first one. We’ll also want some padding around it, so I’ll keep the next immediate circle.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v982142860/brianustas_com/hub3-logo/circle_primitives_center_outline.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v982142860/brianustas_com/hub3-logo/circle_primitives_center_outline.png 2x" width="700" data-dimen="800x800" /></p><p>Next, how to represent the “3” in Hub3? I think of the intersection of <strong>strategy</strong>, <strong>design</strong>, and <strong>code</strong> as a digital product—they all contribute in their own way. I imagine a Venn diagram with three circles that overlap:</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v810749170/brianustas_com/hub3-logo/venn_diagram.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v810749170/brianustas_com/hub3-logo/venn_diagram.png 2x" width="700" data-dimen="800x764" /></p><p>I like the shape created when the circles overlap (outlined in red). It’s a triangle shape and built with three overlapping circles—awesome.</p><p>How do we decide how big the overlapping circles should be? Further, how much should they overlap? To answer those questions quickly, I duplicated the center circle three times (in blue) and rotated them all 120 degrees from each other. I then took the outer-most circle (in green) and moved it up until its edge touched the bottom blue circles.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v484771678/brianustas_com/hub3-logo/venn_diagram_p1.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v484771678/brianustas_com/hub3-logo/venn_diagram_p1.png 2x" width="700" data-dimen="800x881" /></p><p>Next, to create the remaining two overlapping circles, we duplicate the green circle twice and rotate the duplicates 120 degrees around the center, as we did for the blue circles.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v137104309/brianustas_com/hub3-logo/venn_diagram_p2.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v137104309/brianustas_com/hub3-logo/venn_diagram_p2.png 2x" width="700" data-dimen="800x764" /></p><p>It’s coming together. <strong>Note that we haven’t created any new shapes—just clones of existing ones at their original sizes.</strong></p><p>Let’s simplify our workspace and “intersect” the green circles and subtract the inner circle’s padding to get the logo’s outline.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v324411348/brianustas_com/hub3-logo/logo_outline_p1.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v324411348/brianustas_com/hub3-logo/logo_outline_p1.png 2x" width="700" data-dimen="800x800" /></p><p>We now have a circle and a triangle shape. How are we going to represent strategy, design, and code? I decided to divide the triangle shape into three “leafs” surrounding the hub (center circle). To do that, I created a rectangle with the width of the inner circle’s padding and rotated its duplicates around the center.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v688347853/brianustas_com/hub3-logo/logo_outline_p2.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v688347853/brianustas_com/hub3-logo/logo_outline_p2.png 2x" width="700" data-dimen="1085x1169" /></p><p>I then “subtracted” the newly created rectangles (in blue) from the original shape, which results in three new shapes that surround the hub.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v336671360/brianustas_com/hub3-logo/logo_outline_p3.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v336671360/brianustas_com/hub3-logo/logo_outline_p3.png 2x" width="700" data-dimen="800x800" /></p><p>To add a sense of movement to the logo, I decided to accent each of the “leafs” by adding a corner radius to the inner left corner. I then applied Hub3’s branding colors to bring the logo to life.</p><p><img style="box-shadow: none; max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v849119090/brianustas_com/hub3-logo/hub3_logo.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v849119090/brianustas_com/hub3-logo/hub3_logo.png 2x" width="700" data-dimen="800x800" /></p><p>Finally, we need to center the logo in a container for social media. How wide should the logo be relative to the frame? You guessed it—the ratio between the frame’s width and the logo is 1.6:</p><p><img style="max-width: 10rem;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v544178327/brianustas_com/hub3-logo/hub3_square_logo.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v544178327/brianustas_com/hub3-logo/hub3_square_logo.png 2x" width="700" data-dimen="800x800" /></p><h2 id="final-thoughts">Final Thoughts</h2><p>In the end, the Hub3 logo was created almost entirely by cloning circles made via the Golden Ratio and applying <a href="https://www.sketch.com/docs/designing/shapes/boolean-operations/">boolean operations</a>. Via the constraints I outlined above, I moved faster and deliberated less. As they say, sometimes less is more.</p><p>What’s so special about the Golden Ratio? Unfortunately, I don’t know. It was a helpful design heuristic for this project that I’ll likely apply with a grain of salt to future projects. Here are rough alternative logos that don’t use the Golden Ratio—I’ll let you decide if the Golden Ratio helped:</p><p><img style="box-shadow: none; margin: 2rem auto;" alt="" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v781558933/brianustas_com/hub3-logo/hub3_logo_variations.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v781558933/brianustas_com/hub3-logo/hub3_logo_variations.png 2x" width="700" data-dimen="800x131" /></p><p>Interestingly, Spotify found that the Golden Ratio didn’t help their logo:</p><blockquote><p><strong>Gizmodo</strong>: What’s the deal with those waves or squiggles or whatever they are? In the redesign they’ve been moved off to the left.</p><p><strong>CW</strong>: We noticed that people have been complaining on the Interwebs about them not being perfect. <strong>We had tried out the golden ratio on them but that made it look even more weird.</strong> This version is a little more organic and it’s not super perfect, which I think gives it more personality.</p><p><a href="https://gizmodo.com/how-spotify-got-itself-a-new-logo-5995060">How Spotify Got Itself a New Logo</a></p></blockquote>]]></content><author><name>Brian Ustas</name></author><category term="Hub3" /><category term="logo" /><category term="Golden Ratio" /><category term="design constraints" /><category term="Sketch" /><summary type="html"><![CDATA[My process for creating Hub3's logo using the Golden Ratio, which helped me design faster and confidently.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/hub3-logo-golden-ratio.png" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/hub3-logo-golden-ratio.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Preserve Facebook Likes With Nginx</title><link href="https://brianustas.com/blog/preserving-facebook-likes-with-nginx" rel="alternate" type="text/html" title="Preserve Facebook Likes With Nginx" /><published>2019-03-21T00:00:00+00:00</published><updated>2026-11-14T03:37:56+00:00</updated><id>https://brianustas.com/blog/preserving-facebook-likes-with-nginx</id><content type="html" xml:base="https://brianustas.com/blog/preserving-facebook-likes-with-nginx"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Preserve Facebook Likes With Nginx</h1>
    <span class="post-date">Mar 21, 2019</span>
  </div>

  

  
    <ul class="tags">
      <li>Facebook like button</li>
      <li>Nginx</li>
      <li>URL Scheme</li>
      <li>HTTP to HTTPS</li>
      </ul>
  
</div><p>After migrating <a href="https://pandify.com">Pandify</a>’s URL scheme to HTTPS, I noticed that the site’s Facebook likes had disappeared! This post describes how I fixed that.</p><div style="display: flex; flex-direction: row; justify-content: space-around; margin: auto; margin-bottom: -1rem; max-width: 600px;"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_89,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v515319993/brianustas_com/preserving-facebook-likes/pandify-likes-140@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_178,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v515319993/brianustas_com/preserving-facebook-likes/pandify-likes-140@2x.png 2x" width="89" data-dimen="89x62" /><figcaption><strong>http</strong>://pandify.com</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_77,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v112143363/brianustas_com/preserving-facebook-likes/pandify-likes-0@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_154,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v112143363/brianustas_com/preserving-facebook-likes/pandify-likes-0@2x.png 2x" width="77" data-dimen="77x62" /><figcaption><strong>https</strong>://pandify.com</figcaption></figure></div><h2 id="background">Background</h2><p>When configuring the Facebook like button you specify the ‘URL to like,’ which is the URL that new likes will get attributed to:</p><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_274,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v525126153/brianustas_com/preserving-facebook-likes/like-button-configurator@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_548,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v525126153/brianustas_com/preserving-facebook-likes/like-button-configurator@2x.png 2x" width="274" data-dimen="274x122" /><figcaption><a href="https://developers.facebook.com/docs/plugins/like-button">Facebook Like Button Configurator</a></figcaption></figure><p><strong>However, the like button’s count reflects the likes for the current page’s URL, and NOT for the URL you configured the like button with</strong>. If a page’s URL changes at all, including the URL scheme, that page’s like button count will change to reflect the new URL’s like count.</p><p>At a high level, here’s what I wanted to do:</p><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301">301 redirect</a> HTTP to HTTPS</li><li>Preserve Facebook likes from the non-HTTPS URL</li><li>Allow new likes from the HTTPS URL to increase the like count</li></ul><h2 id="solution">Solution</h2><p>Facebook shares <a href="https://web.archive.org/web/20190403091233/https://developers.facebook.com/docs/plugins/faqs#moving-urls">some insight</a> on this issue:</p><blockquote><p>…you can’t move the likes, shares or comments directly to the new URL but you can use the old URL as the canonical source for the number of likes or shares at the new URL.</p></blockquote><p>There are three ways to declare the ‘canonical source’ to Facebook:</p><ol type="1"><li>HTTP 301 or 302 redirect</li><li><code class="xml">&lt;meta property="og:url" content="..." /&gt;</code></li><li><code class="xml">&lt;link rel="canonical" href="..."&gt;</code></li></ol><p>Option #1 is a no-go because Pandify’s Nginx server redirects HTTP to HTTPS. Again, this is what causes the Facebook likes to disappear.</p><p>I want the <a href="https://en.wikipedia.org/wiki/Canonical_link_element">canonical URL</a> to be the HTTPS URL. Left with option #2, I added a <code>&lt;meta&gt;</code> tag to Pandify’s HTML template that points to the non-HTTPS URL (which has the Facebook likes associated with it).</p><div class="no-wrap-code"><pre class="xml"><code>&lt;!-- Preserve Facebook likes from the non-HTTPS URL. --&gt;
&lt;meta property=&quot;og:url&quot; content=&quot;http://pandify.com&quot; /&gt;</code></pre></div><p>This tag tells Facebook to use the non-HTTPS URL as the ‘canonical source’ for likes. However, this doesn’t restore the likes yet, and <a href="https://developers.facebook.com/tools/debug/sharing">Facebook’s Sharing Debugger</a> shows a new problem when debugging <code>https://pandify.com</code>:</p><blockquote><p>Circular Redirect: We could not resolve the canonical URL because the redirect path contained a cycle.</p></blockquote><p>What’s happening? Facebook’s scraper is following the <code>&lt;meta&gt;</code> tag’s <code>og:url</code> (the non-HTTPS URL) and getting redirected by Nginx to the HTTPS URL, causing a redirect loop!</p><p>My solution is to detect the Facebook scraper’s <a href="https://en.wikipedia.org/wiki/User_agent">user agent</a> and NOT redirect to the HTTPS site, but instead return <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200">200 OK</a>. I do this via Pandify’s Nginx server block configuration for non-HTTPS requests:</p><div class="no-wrap-code"><pre class="nginx"><code>server {
  listen 80;
  server_name pandify.com www.pandify.com;

  location / {
    if ($http_user_agent ~* &quot;facebookexternalhit&quot;) {
      # Preserve Facebook likes from the non-HTTPS URL.
      add_header Content-Type text/html;
      # NOTE: Add other Open Graph tags to keep Facebook&#39;s Sharing Debugger happy.
      return 200 &#39;&lt;meta property=&quot;og:url&quot; content=&quot;http://pandify.com&quot; /&gt;&#39;;
    }
    return 301 https://pandify.com$request_uri;
  }
}</code></pre></div><p>Voila! After restarting Nginx and forcing <a href="https://developers.facebook.com/tools/debug/sharing">Facebook’s Sharing Debugger</a> to ‘Scrape Again’ for <code>https://pandify.com</code>, the likes from <code>http://pandify.com</code> returned.</p><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_457,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v406449152/brianustas_com/preserving-facebook-likes/pandify-sharing-debugger@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_914,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v406449152/brianustas_com/preserving-facebook-likes/pandify-sharing-debugger@2x.png 2x" width="457" data-dimen="457x230" /><figcaption><code>https://pandify.com</code> debug info</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="Facebook like button" /><category term="Nginx" /><category term="URL Scheme" /><category term="HTTP to HTTPS" /><summary type="html"><![CDATA[How to preserve Facebook likes after migrating the URL scheme from HTTP to HTTPS.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/preserving-facebook-likes.png" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/preserving-facebook-likes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Cryptocurrency Pairs Trading</title><link href="https://brianustas.com/blog/cryptocurrency-pairs-trading" rel="alternate" type="text/html" title="Cryptocurrency Pairs Trading" /><published>2018-09-06T00:00:00+00:00</published><updated>2026-11-14T03:37:56+00:00</updated><id>https://brianustas.com/blog/cryptocurrency-pairs-trading</id><content type="html" xml:base="https://brianustas.com/blog/cryptocurrency-pairs-trading"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Cryptocurrency Pairs Trading</h1>
    <span class="post-date">Sep 6, 2018</span>
  </div>

  

  
    <ul class="tags">
      <li>Python</li>
      <li>NumPy</li>
      <li>Pandas</li>
      <li>Matplotlib</li>
      <li>Scikit-learn</li>
      <li>statistics</li>
      <li>machine learning</li>
      <li>algorithmic trading</li>
      <li>pairs trading</li>
      <li>cryptocurrency</li>
      </ul>
  
</div><p>During 2017’s Bitcoin bull run, the price of many cryptocurrencies changed rapidly. Bitcoin grew to nearly $20,000, and the high levels of excitement and <a href="https://en.wikipedia.org/wiki/Fear_of_missing_out">FOMO</a> created many arbitrage opportunities. This is also when my barber started talking about Bitcoin…</p><p>With friends, I took advantage of the US–Korea arbitrage opportunity as well as built a simple arbitrage bot in Ruby that watched GDAX and Kraken for price differences. Those opportunities don’t seem to be easily exploitable now.</p><p>One takeaway from that experience is that cryptocurrency trading is driven largely by speculation—many don’t have real-world adoption yet. Given that many cryptocurrencies respond to similar market pressures and regulation, I wondered if prices moved together in any predictable way.</p><p>This led me to the idea of <a href="https://www.investopedia.com/university/guide-pairs-trading/">Pairs Trading</a>. The idea is simple: find a pair of assets whose prices “move together” and bet against deviations in that relationship. How you confidently define the pair’s relationship is one of the hard problems.</p><p>After studying the theory behind Pairs Trading and reading a few papers, I built a high-frequency statistical arbitrage bot and backtesting pipeline. I implemented Distance and Cointegration Pairs Trading strategies on Poloniex’s Margin Trading Exchange. It looks for cryptocurrencies whose prices move together and bets against trend anomalies. It’s built with <a href="https://www.python.org/">Python</a>, <a href="http://www.numpy.org">NumPY</a>, <a href="https://pandas.pydata.org">Pandas</a>, <a href="https://matplotlib.org">Matplotlib</a>, <a href="https://scikit-learn.org">Scikit-learn</a>, <a href="https://github.com/mrjbq7/ta-lib">TA-Lib</a>, and <a href="https://github.com/coleifer/peewee">Peewee</a>.</p><p>Key resources:</p><ul><li><a href="https://hdl.handle.net/11250/221265">Statistical Arbitrage: High-Frequency Pairs Trading</a></li><li><a href="http://docs.finance.free.fr/DOCS/Yats/cointegration-en%5B1%5D.pdf">Pairs Trading, Convergence Trading, Cointegration</a></li></ul><h2 id="screenshots">Screenshots</h2><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v740608088/brianustas_com/cryptocurrency-pairs-trading/v1-trader.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v740608088/brianustas_com/cryptocurrency-pairs-trading/v1-trader.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v740608088/brianustas_com/cryptocurrency-pairs-trading/v1-trader.png" data-dimen="1302x1073" /><figcaption>V1: Pairs Trading + machine learning on technical indicators.</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v79190761/brianustas_com/cryptocurrency-pairs-trading/v2-trader.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v79190761/brianustas_com/cryptocurrency-pairs-trading/v2-trader.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v79190761/brianustas_com/cryptocurrency-pairs-trading/v2-trader.png" data-dimen="1598x945" /><figcaption>V2: Technical indicators aren’t very helpful. Revert to basic Pairs Trading.</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v960316926/brianustas_com/cryptocurrency-pairs-trading/v2-backtest-logs.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v960316926/brianustas_com/cryptocurrency-pairs-trading/v2-backtest-logs.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v960316926/brianustas_com/cryptocurrency-pairs-trading/v2-backtest-logs.png" data-dimen="713x766" /><figcaption>backtest logs</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_666,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v820388548/brianustas_com/cryptocurrency-pairs-trading/source-code-files.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1332,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v820388548/brianustas_com/cryptocurrency-pairs-trading/source-code-files.png 2x" width="666" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v820388548/brianustas_com/cryptocurrency-pairs-trading/source-code-files.png" data-dimen="666x1000" /><figcaption>Python source code files (source code is private)</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="Python" /><category term="NumPy" /><category term="Pandas" /><category term="Matplotlib" /><category term="Scikit-learn" /><category term="statistics" /><category term="machine learning" /><category term="algorithmic trading" /><category term="pairs trading" /><category term="cryptocurrency" /><summary type="html"><![CDATA[High-frequency quantitative trading bot implementing statistical arbitrage strategies on a cryptocurrency exchange.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/crypto-pairs-trader.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/crypto-pairs-trader.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">‘Structure from Motion’ Pipeline</title><link href="https://brianustas.com/blog/structure-from-motion-pipeline" rel="alternate" type="text/html" title="‘Structure from Motion’ Pipeline" /><published>2017-09-16T00:00:00+00:00</published><updated>2026-11-14T03:37:56+00:00</updated><id>https://brianustas.com/blog/structure-from-motion-pipeline</id><content type="html" xml:base="https://brianustas.com/blog/structure-from-motion-pipeline"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>‘Structure from Motion’ Pipeline</h1>
    <span class="post-date">Sep 16, 2017</span>
  </div>

  

  
    <ul class="tags">
      <li>Ruby</li>
      <li>Docker</li>
      <li>CLI</li>
      <li>OpenMVG</li>
      <li>OpenMVS</li>
      <li>SfM</li>
      <li>photogrammetry</li>
      </ul>
  
</div><p>Dockerized CLI tool for quickly generating 3D models from a set of images using <a href="https://github.com/openMVG/openMVG">OpenMVG</a> and <a href="https://github.com/cdcseacave/openMVS">OpenMVS</a>.</p><p>I built this to tackle the problem of <a href="https://www.ada.gov/">ADA</a> incompliant curb ramps. With photogrammetry, curb ramps can be more quickly surveyed and later fixed.</p><p>The goal is to use smartphone photos to generate 3D curb ramp models which can be measured virtually. The current process involves many tedious hand measurements.</p><p>Example usage:</p><pre class="shell"><code>./bin/mvg_mvs_pipeline --image-dir &lt;image-dir&gt; --ouput-dir &lt;output-dir&gt;</code></pre><h2 id="screenshots">Screenshots</h2><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v23547707/brianustas_com/structure-from-motion-pipeline/input-curb-ramp-collage.jpg" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v23547707/brianustas_com/structure-from-motion-pipeline/input-curb-ramp-collage.jpg 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v23547707/brianustas_com/structure-from-motion-pipeline/input-curb-ramp-collage.jpg" data-dimen="2419x907" /><figcaption>Input photos of a curb ramp.</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v562446064/brianustas_com/structure-from-motion-pipeline/output-curb-ramp-model.jpg" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v562446064/brianustas_com/structure-from-motion-pipeline/output-curb-ramp-model.jpg 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v562446064/brianustas_com/structure-from-motion-pipeline/output-curb-ramp-model.jpg" data-dimen="914x504" /><figcaption>Output curb ramp model.</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="Ruby" /><category term="Docker" /><category term="CLI" /><category term="OpenMVG" /><category term="OpenMVS" /><category term="SfM" /><category term="photogrammetry" /><summary type="html"><![CDATA[Dockerized CLI tool for quickly generating 3D models from a set of images using [OpenMVG](https://github.com/openMVG/openMVG) and [OpenMVS](https://github.com/cdcseacave/openMVS).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/sfm-pipeline.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/sfm-pipeline.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hitpic, LLC</title><link href="https://brianustas.com/blog/hitpic" rel="alternate" type="text/html" title="Hitpic, LLC" /><published>2017-02-04T00:00:00+00:00</published><updated>2026-11-14T03:37:56+00:00</updated><id>https://brianustas.com/blog/hitpic</id><content type="html" xml:base="https://brianustas.com/blog/hitpic"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Hitpic, LLC</h1>
    <span class="post-date">Feb 4, 2017</span>
  </div>

  
    <ul class="links">
      <li><a href="https://hitpic.me">website</a></li>
      </ul>
  

  
    <ul class="tags">
      <li>JavaScript</li>
      <li>Ruby on Rails</li>
      <li>Objective-C</li>
      <li>React Native</li>
      <li>Redux</li>
      <li>PostgreSQL</li>
      <li>Redis</li>
      <li>Sketch</li>
      <li>image editing</li>
      <li>geolocation</li>
      <li>app design</li>
      </ul>
  
</div><p>Hitpic is a competitive, location-based photo sharing iOS app. Each day at 8 PM ET, yesterday’s top-rated <em>pics</em> at each location become <em>hitpics!</em> Increase your score and global ranking by earning ratings and hitpic awards.</p><p>More details are on <a href="https://hitpic.me">Hitpic’s website</a>.</p><h2 id="technology">Technology</h2><p>I designed Hitpic with <a href="https://www.sketchapp.com/">Sketch</a> and built the app’s frontend with <a href="https://reactnative.dev/">React Native</a>, a framework for building native iOS apps with JavaScript. For image editing and filtering, I wrote custom Objective-C modules for performance reasons. Frontend state is managed with <a href="https://redux.js.org/">Redux</a>.</p><p>The backend’s built with <a href="https://rubyonrails.org/">Ruby on Rails</a> and <a href="https://www.postgresql.org/">PostgreSQL</a>. <a href="https://redis.io/">Redis</a> is used for background processing with <a href="https://sidekiq.org/">Sidekiq</a>. I also developed an admin interface with <a href="https://activeadmin.info/">Active Admin</a> for monitoring users, content, and marketing effectiveness. <a href="https://crashlytics.com">Crashlytics</a> was used for distributing beta testing builds.</p><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v320781247/brianustas_com/hitpic/frontend-source-code-files.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v320781247/brianustas_com/hitpic/frontend-source-code-files.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v320781247/brianustas_com/hitpic/frontend-source-code-files.png" data-dimen="841x1159" /><figcaption>frontend source code files (source code is private)</figcaption></figure><h2 id="screenshots">Screenshots</h2><div class="row"><div class="col-md-6" style="display: flex; align-items: center;"><!-- center vertically --><figure><img class="ios-app-icon" src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_512,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v747073770/brianustas_com/hitpic/app-icon@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1024,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v747073770/brianustas_com/hitpic/app-icon@2x.png 2x" width="512" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v747073770/brianustas_com/hitpic/app-icon@2x.png" data-dimen="512x512" /><figcaption>iOS app icon</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_473,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v375253424/brianustas_com/hitpic/screenshot-1@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_946,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v375253424/brianustas_com/hitpic/screenshot-1@2x.png 2x" width="473" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v375253424/brianustas_com/hitpic/screenshot-1@2x.png" data-dimen="473x842" /><figcaption>View pics near your location.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_473,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v380596821/brianustas_com/hitpic/screenshot-2@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_946,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v380596821/brianustas_com/hitpic/screenshot-2@2x.png 2x" width="473" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v380596821/brianustas_com/hitpic/screenshot-2@2x.png" data-dimen="473x842" /><figcaption>View hitpics near your location.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_473,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v854021074/brianustas_com/hitpic/screenshot-3@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_946,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v854021074/brianustas_com/hitpic/screenshot-3@2x.png 2x" width="473" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v854021074/brianustas_com/hitpic/screenshot-3@2x.png" data-dimen="473x842" /><figcaption>user profile and alerts</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_473,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v133688295/brianustas_com/hitpic/screenshot-4@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_946,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v133688295/brianustas_com/hitpic/screenshot-4@2x.png 2x" width="473" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v133688295/brianustas_com/hitpic/screenshot-4@2x.png" data-dimen="473x842" /><figcaption>Photo editing similar to Instagram.</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_473,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v366124987/brianustas_com/hitpic/screenshot-5@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_946,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v366124987/brianustas_com/hitpic/screenshot-5@2x.png 2x" width="473" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v366124987/brianustas_com/hitpic/screenshot-5@2x.png" data-dimen="473x842" /><figcaption>Explore pics from around the world.</figcaption></figure></div></div><h2 id="marketing-material">Marketing Material</h2><div class="row"><div class="col-md-12"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v58125668/brianustas_com/hitpic/website.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v58125668/brianustas_com/hitpic/website.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v58125668/brianustas_com/hitpic/website.png" data-dimen="1278x982" /><figcaption><a href="https://hitpic.me">https://hitpic.me</a></figcaption></figure></div><div class="col-md-12"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v895803434/brianustas_com/hitpic/ad-1@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v895803434/brianustas_com/hitpic/ad-1@2x.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v895803434/brianustas_com/hitpic/ad-1@2x.png" data-dimen="820x312" /><figcaption>Facebook ad 1</figcaption></figure></div><div class="col-md-12"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v791832359/brianustas_com/hitpic/ad-2@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v791832359/brianustas_com/hitpic/ad-2@2x.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v791832359/brianustas_com/hitpic/ad-2@2x.png" data-dimen="828x315" /><figcaption>Facebook ad 2</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v338863155/brianustas_com/hitpic/carousel-ad-1@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v338863155/brianustas_com/hitpic/carousel-ad-1@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v338863155/brianustas_com/hitpic/carousel-ad-1@2x.png" data-dimen="621x621" /><figcaption>carousel ad 1</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v411999482/brianustas_com/hitpic/carousel-ad-2@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v411999482/brianustas_com/hitpic/carousel-ad-2@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v411999482/brianustas_com/hitpic/carousel-ad-2@2x.png" data-dimen="621x621" /><figcaption>carousel ad 2</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v648403597/brianustas_com/hitpic/carousel-ad-3@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v648403597/brianustas_com/hitpic/carousel-ad-3@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v648403597/brianustas_com/hitpic/carousel-ad-3@2x.png" data-dimen="621x621" /><figcaption>carousel ad 3</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v228948723/brianustas_com/hitpic/carousel-ad-4@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v228948723/brianustas_com/hitpic/carousel-ad-4@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v228948723/brianustas_com/hitpic/carousel-ad-4@2x.png" data-dimen="621x621" /><figcaption>carousel ad 4</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v748338151/brianustas_com/hitpic/carousel-ad-5@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v748338151/brianustas_com/hitpic/carousel-ad-5@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v748338151/brianustas_com/hitpic/carousel-ad-5@2x.png" data-dimen="621x621" /><figcaption>carousel ad 5</figcaption></figure></div><div class="col-md-6"><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_621,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v233929365/brianustas_com/hitpic/carousel-ad-6@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1242,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v233929365/brianustas_com/hitpic/carousel-ad-6@2x.png 2x" width="621" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v233929365/brianustas_com/hitpic/carousel-ad-6@2x.png" data-dimen="621x621" /><figcaption>carousel ad 6</figcaption></figure></div></div><h2 id="old-designs">Old Designs</h2><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v874465076/brianustas_com/hitpic/old-design-board.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v874465076/brianustas_com/hitpic/old-design-board.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v874465076/brianustas_com/hitpic/old-design-board.png" data-dimen="1546x1296" /><figcaption>first design sheet</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_569,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v446768077/brianustas_com/hitpic/app-icon-ideas@2x.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1138,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v446768077/brianustas_com/hitpic/app-icon-ideas@2x.png 2x" width="569" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v446768077/brianustas_com/hitpic/app-icon-ideas@2x.png" data-dimen="569x388" /><figcaption>app icon ideas</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="JavaScript" /><category term="Ruby on Rails" /><category term="Objective-C" /><category term="React Native" /><category term="Redux" /><category term="PostgreSQL" /><category term="Redis" /><category term="Sketch" /><category term="image editing" /><category term="geolocation" /><category term="app design" /><summary type="html"><![CDATA[Competitive, location-based photo sharing app. Rate photos with emoji and yesterday's top-rated pics become hitpics!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/hitpic.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/hitpic.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Knightly Dashboard Demo</title><link href="https://brianustas.com/blog/knightly-dashboard-demo" rel="alternate" type="text/html" title="Knightly Dashboard Demo" /><published>2015-09-23T00:00:00+00:00</published><updated>2026-11-14T01:21:46+00:00</updated><id>https://brianustas.com/blog/knightly-dashboard-demo</id><content type="html" xml:base="https://brianustas.com/blog/knightly-dashboard-demo"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Knightly Dashboard Demo</h1>
    <span class="post-date">Sep 23, 2015</span>
  </div>

  
    <ul class="links">
      <li><a href="https://brianustas.com/knightly-dashboard-demo/">demo</a></li>
      <li><a href="https://github.com/ustasb/knightly_partners_dashboard">source code</a></li>
      </ul>
  

  
    <ul class="tags">
      <li>JavaScript</li>
      <li>Webpack</li>
      <li>React</li>
      <li>Flux</li>
      <li>Google Maps</li>
      <li>simulation</li>
      <li>app design</li>
      </ul>
  
</div><p>Dashboard for campus police dispatchers that shows students in distress or panic, and the officers that are responding to incidents.</p><p>All the data is fake and randomly generated. For demonstration, many incidents are always occurring.</p><p>Knightly went on to raise funding from <a href="https://www.northeastern.edu/idea/">Northeastern’s IDEA</a> program with this MVP. Read more about Knightly <a href="https://www.f6s.com/knightlyllc/about">here</a>.</p><h2 id="features">Features</h2><ul><li>A live Google Maps instance around Northeastern University.</li><li>As the student moves, their previous positions are recorded on the map.</li><li>Officers that are following students are connected via a line.</li><li>Press <code>s</code> to stop/start the simulation.</li><li>Hold <code>&lt;Space&gt;</code> to increase the simulation speed.</li><li>Click on a map marker to view information about the user.</li><li>Clicking on any side menu feed item will focus the map appropriately.</li><li>Live Feed Menu:<ul><li>Shows the latest event data.</li></ul></li><li>Incidents Menu:<ul><li>Shows the currently panicked and distressed students.</li><li>Shows the last 5 resolved student incidents.</li></ul></li></ul><h2 id="screenshots">Screenshots</h2><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v12274835/brianustas_com/knightly-dashboard-demo/live-feed.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v12274835/brianustas_com/knightly-dashboard-demo/live-feed.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v12274835/brianustas_com/knightly-dashboard-demo/live-feed.png" data-dimen="1280x800" /><figcaption>live feed menu</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v887623791/brianustas_com/knightly-dashboard-demo/incidents.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v887623791/brianustas_com/knightly-dashboard-demo/incidents.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v887623791/brianustas_com/knightly-dashboard-demo/incidents.png" data-dimen="1280x800" /><figcaption>incidents menu</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="JavaScript" /><category term="Webpack" /><category term="React" /><category term="Flux" /><category term="Google Maps" /><category term="simulation" /><category term="app design" /><summary type="html"><![CDATA[Dashboard for campus police dispatchers that shows students in distress or panic, and the officers that are responding to incidents.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/knightly-dashboard-demo.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/knightly-dashboard-demo.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Where in the World</title><link href="https://brianustas.com/blog/where-in-the-world" rel="alternate" type="text/html" title="Where in the World" /><published>2015-01-04T00:00:00+00:00</published><updated>2026-11-14T01:21:46+00:00</updated><id>https://brianustas.com/blog/where-in-the-world</id><content type="html" xml:base="https://brianustas.com/blog/where-in-the-world"><![CDATA[<!-- Without {=html}, Pandoc formats the HTML creating inline-block spacing issues. -->
<div class="post-header">
  <div class="title">
    <h1>Where in the World</h1>
    <span class="post-date">Jan 4, 2015</span>
  </div>

  
    <ul class="links">
      <li><a href="https://brianustas.com/where-in-the-world/">demo</a></li>
      <li><a href="https://github.com/ustasb/where_in_the_world">source code</a></li>
      </ul>
  

  
    <ul class="tags">
      <li>JavaScript</li>
      <li>jQuery</li>
      <li>jVectorMap</li>
      <li>app design</li>
      <li>educational quiz</li>
      </ul>
  
</div><p>Educational quiz to aid the learning of world country locations and capitals. Little planes fly around for entertainment!</p><p>I built this because I wanted a more interactive, visual way to study geography. I also couldn’t find quality quizzes for testing knowledge of country locations on a map.</p><h2 id="screenshots">Screenshots</h2><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v724727699/brianustas_com/where-in-the-world/intro.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v724727699/brianustas_com/where-in-the-world/intro.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v724727699/brianustas_com/where-in-the-world/intro.png" data-dimen="1204x660" /><figcaption>quiz selection menu</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v595597390/brianustas_com/where-in-the-world/world-capitals.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v595597390/brianustas_com/where-in-the-world/world-capitals.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v595597390/brianustas_com/where-in-the-world/world-capitals.png" data-dimen="1204x655" /><figcaption>world country capitals quiz</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_700,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v606163087/brianustas_com/where-in-the-world/usa-locations.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1400,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v606163087/brianustas_com/where-in-the-world/usa-locations.png 2x" width="700" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v606163087/brianustas_com/where-in-the-world/usa-locations.png" data-dimen="1208x640" /><figcaption>USA state locations quiz</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_390,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v734253481/brianustas_com/where-in-the-world/spelling.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_780,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v734253481/brianustas_com/where-in-the-world/spelling.png 2x" width="390" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v734253481/brianustas_com/where-in-the-world/spelling.png" data-dimen="390x160" /><figcaption><a href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein distance</a> is used to tolerate spelling mistakes.</figcaption></figure><figure><img src="https://res.cloudinary.com/dkqzmhoav/image/upload/w_596,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v963633974/brianustas_com/where-in-the-world/planes.png" srcset="https://res.cloudinary.com/dkqzmhoav/image/upload/w_1192,c_limit,q_auto:best,f_auto,fl_lossy,fl_progressive/v963633974/brianustas_com/where-in-the-world/planes.png 2x" width="596" data-photoswipe="https://res.cloudinary.com/dkqzmhoav/image/upload/q_auto:best,f_auto,fl_lossy,fl_progressive/v963633974/brianustas_com/where-in-the-world/planes.png" data-dimen="596x400" /><figcaption>Little planes (not this many) fly around!</figcaption></figure>]]></content><author><name>Brian Ustas</name></author><category term="JavaScript" /><category term="jQuery" /><category term="jVectorMap" /><category term="app design" /><category term="educational quiz" /><summary type="html"><![CDATA[Educational quiz to aid the learning of world country locations and capitals. Little planes fly around for entertainment!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://brianustas.com/assets/images/thumbnails/where-in-the-world.jpg" /><media:content medium="image" url="https://brianustas.com/assets/images/thumbnails/where-in-the-world.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>