<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[Devollox]]></title>
        <description><![CDATA[Devollox]]></description>
        <link>https://devollox.fun/</link>
        <image>
            <url>https://github.com/Devollox/Devollox-Website/blob/main/public/logo512.png</url>
            <title>Devollox</title>
            <link>https://devollox.fun/</link>
        </image>
        <generator>RSS for Node</generator>
        <lastBuildDate>Fri, 01 May 2026 07:14:32 GMT</lastBuildDate>
        <atom:link href="https://devollox.fun/feed.xml" rel="self" type="application/rss+xml"/>
        <language><![CDATA[en]]></language>
        <item>
            <title><![CDATA[Real-time in a portfolio]]></title>
            <description><![CDATA[<p>Most portfolios behave like static business cards: some text, a few screenshots, a couple of links. For this project, the goal was to also show how the interface behaves <em>in the moment</em> — when several people land on the site at the same time. To do that, a few small real-time details were added on top of the usual pages: live cursors, presence counters, and synced interactions.</p>
<h2>Live presence on About</h2>
<p>The About page has a thin layer of social presence. When someone visits it, small cursor markers appear on top of the layout, similar to what you would expect from tools like Figma or online whiteboards. They never block the text or scrolling, but they make it clear that you are not the only person exploring the page.</p>
<p>Technically, this is a room-based presence system that listens for cursor updates and keeps a shared list of active peers. Each client publishes throttled cursor events and subscribes to updates from others.</p>
<h2>Synchronized signatures</h2>
<p>On the Signature page, multiple people can sign at the same time. When a new stroke is added to the canvas on one screen, it appears on everyone else’s screens in real time — no reload, no manual refresh. For visitors, the page feels like a lightweight collaborative wall dedicated to signatures rather than a static gallery.</p>
<p>Under the hood, each signature has an id, a list of points, and some metadata that gets sent over a real-time channel and stored in a backing store. Clients subscribe to changes: when a signature is created or removed, the updated state is broadcast to all connected users.</p>
<h2>Why add real-time to a portfolio</h2>
<p>Real-time details in a portfolio solve two problems at once. On one hand, they add a bit of playfulness and energy, making the site feel less predictable and more alive. On the other, they work as proof of skills: handling WebSocket connections, presence, state sync and performance constraints.</p>
<p>Instead of spinning up a separate “real-time demo” project, the live layer is embedded directly into the foundation of the personal site. This way, a recruiter or team lead sees not just a list of technologies on a resume, but also how those technologies behave in a real interface — quietly, consistently and with just enough personality to spark the question: “So how does this actually work under the hood?”</p>
]]></description>
            <link>https://devollox.fun/blog/realtime-details</link>
            <guid isPermaLink="false">https://devollox.fun/blog/realtime-details</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Footer as a Glow Tuner]]></title>
            <description><![CDATA[<p>Most footers quietly sit at the bottom of the page with a logo, a copyright line and a couple of links. In this project, the footer became a tiny control panel instead: a place where the visitor can nudge the look of the site without wading through settings.</p>
<p>The idea was to hide a glow color switcher behind the personal mark in the footer. A small Ⓓ button opens a minimal modal where the user picks one of several glow presets. The choice subtly adjusts accent highlights across the layout and persists between visits, so the site remembers the preferred glow.</p>
<h2>Making the footer interactive</h2>
<p>The footer is rendered on every page and shows a compact brand block with a mark, a title and a short tagline, followed by social links and the usual copyright line. The mark itself is not just decoration; it is a button that toggles the glow modal. This keeps the footer visually calm while still letting power users discover a bit of customization.</p>
<p>In JSX, the mark is just a button wired to a local <code>showGlowModal</code> state:</p>
<pre><code class="language-javascript">const [showGlowModal, setShowGlowModal] = useState(false)

&lt;button
type=&quot;button&quot;
className={styles.footer_mark}
onClick={() =&gt; setShowGlowModal(true)}

&lt;span&gt;Ⓓ&lt;/span&gt;
&lt;/button&gt;
</code></pre>
<p>When the modal is open, it presents a small list of glow options that depend on the current theme. In light mode, the palette offers deeper, charcoal‑style tones; in dark mode, it shifts to lighter, sand and stone‑like accents that stand out on a dark background. The options are defined as simple objects with a readable label and RGB values for both the glow and its swatch.</p>
<h2>Wiring glow to the theme</h2>
<p>The footer reads the current theme from <code>next-themes</code> and selects the appropriate set of glow presets. This allows the same interaction model to work in both light and dark modes without extra branching in the JSX. If the theme changes, the list of available glow colors updates automatically and stays in sync with the rest of the design system.</p>
<p>A small hook around <code>useTheme</code> keeps the options list in sync with the active theme:</p>
<pre><code class="language-javascript">const { theme } = useTheme()

const options = useMemo(() =&gt; {
  if (theme === &#39;light&#39;) return GLOW_OPTIONS_LIGHT
  return GLOW_OPTIONS_DARK
}, [theme])
</code></pre>
<p>Each glow preset is applied by updating a few CSS custom properties on the <code>document.documentElement</code>: a solid <code>--glow-color</code>, a softer <code>--glow-soft</code>, and a <code>--glow-shadow</code> used for shadows and highlights. Because these are just variables, existing components can consume them without knowing anything about the footer logic, and the accent can be reused in multiple places.</p>
<pre><code class="language-javascript">const applyGlow = (option: GlowOption) =&gt; {
const root = document.documentElement
	root.style.setProperty(&#39;--glow-color&#39;, rgba(${option.rgb}, 1))
	root.style.setProperty(&#39;--glow-soft&#39;, rgba(${option.rgb}, 0.35))
	root.style.setProperty(&#39;--glow-shadow&#39;, rgba(${option.rgb}, 0.3))
}
</code></pre>
<h2>Remembering the user&#39;s choice</h2>
<p>To avoid resetting the glow on every page load, the footer stores the selected preset key in <code>localStorage</code>. On mount, an effect reads <code>glow-color</code>, matches it against the current preset list and applies the corresponding values; if nothing is saved yet, the first option in the list becomes the default. This means the glow feels like part of the user’s personal setup rather than a one‑off toggle.</p>
<pre><code class="language-javascript">useEffect(() =&gt; {
  if (typeof window === &#39;undefined&#39;) return

  const saved = window.localStorage.getItem(&#39;glow-color&#39;)
  const currentOptions = options

  let option = saved ? currentOptions.find(o =&gt; o.value === saved) : undefined

  if (!option) {
    option = currentOptions
  }

  applyGlow(option)
}, [options])
</code></pre>
<p>Switching between presets is handled by a single helper. It looks up the option by value, updates the CSS variables, writes the new value to <code>localStorage</code> and closes the modal. The rest of the layout automatically reacts to the variable change, so there is no need to rerender large parts of the tree.</p>
<pre><code class="language-javascript">const setGlow = (value: string) =&gt; {
  if (typeof document === &#39;undefined&#39;) return

  const option = options.find(o =&gt; o.value === value)
  if (!option) return

  applyGlow(option)
  window.localStorage.setItem(&#39;glow-color&#39;, value)
  setShowGlowModal(false)
}
</code></pre>
<h2>Hinting that the footer is adjustable</h2>
<p>Because the control is intentionally subtle, the footer gives a small hint that the mark is interactive. A short line under the brand block can invite exploration, for example: “Tap the Ⓓ mark to adjust the glow.” This keeps the microcopy light and optional while still guiding users who are curious about personalization.</p>
<p>The mark itself can also have a very gentle pulse or halo on the first visit to suggest that it does something, then settle into a static state afterwards. Paired together, a one‑line hint and a soft motion cue make the footer feel like a quiet, discoverable playground for fine‑tuning the site’s atmosphere instead of just a static block at the bottom of the page.</p>
]]></description>
            <link>https://devollox.fun/blog/footer-glow-tuner</link>
            <guid isPermaLink="false">https://devollox.fun/blog/footer-glow-tuner</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Shaping the Footer]]></title>
            <description><![CDATA[<p>The original footer on this site was just a dark strip with a few links. It worked, but it did not really say anything about whose site this is. The goal was to turn the bottom of the page into something a bit more intentional: not just a place for links, but a small signature that says “this was built by Devollox.”</p>
<p>The new footer sits on a dark background, aligned to the same content width and separated by a thin border line. Inside, it is split into two simple blocks: brand on the left, external links on the right. This keeps the structure clean and avoids the usual overloaded multi‑column footer pattern.</p>
<p>The brand block is a small badge with the Ⓓ mark, the Devollox name, and a short tagline. The badge has a subtle outline and a soft glow, which adds a neon‑like accent without turning the footer into a light show. In light and dark themes it keeps the same shape, but adjusts glow and text contrast so it feels at home in both.</p>
<p>On the right there is a row of text links: Steam, GitHub, Stack Overflow, and Reddit. They are styled as calm, lowercase text instead of buttons: secondary color, gentle hover state with a tiny lift. That way the footer stays light, and those external profiles feel like part of the personal brand rather than a block of social icons.</p>
<p>On the home page the footer uses a slightly different, lighter shade of dark, which creates a soft “platform” at the end of the layout instead of a hard cut to black. In the end, the bottom of the site is no longer just boilerplate — it behaves like a quiet, persistent signature that closes every page with the same voice.</p>
]]></description>
            <link>https://devollox.fun/blog/footer-refresh</link>
            <guid isPermaLink="false">https://devollox.fun/blog/footer-refresh</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building the Signature Page]]></title>
            <description><![CDATA[<p>When I finished the main pages of the site, it still felt a bit too static. A portfolio shows who you are, but I wanted a place where people could leave something of their own. That idea slowly turned into the <strong>Signature</strong> page.</p>
<p>Instead of a classic guestbook with text inputs, I decided to let people draw their actual signatures. The page uses <code>next-auth</code> to identify the current user and Firebase to store every signature as a base64-encoded PNG together with the author name and timestamp.</p>
<p>The main <code>Signature</code> page loads all signatures from Firebase, sorts them by timestamp and shows them in small cards. The list is paginated: first 10 entries are visible, and a “Show More” button loads the next batch without reloading the page. Each user can have only one signature: if you are signed in and your name is already in the list, the “Create Signature” button is hidden.</p>
<p>The drawing happens on a <code>&lt;canvas&gt;</code> element. Mouse events (<code>mousedown</code>, <code>mousemove</code>, <code>mouseup</code>) are used to track the stroke and draw smooth lines with round caps and joins. Every finished stroke is saved into an in‑memory history array as a data URL, which makes it possible to implement a simple <code>Ctrl+Z</code> undo by restoring the previous canvas state.</p>
<p>When the user clicks “Accept Signature”, the canvas is converted to a PNG data URL, stripped to a base64 string and saved to Firebase together with the user name and current ISO timestamp. On the frontend, the new signature is immediately added to the top of the list, so the user sees the result without waiting for a full reload.</p>
<p>Each signature card renders the stored image via a <code>data:image/png;base64,...</code> source and shows the author’s name with a formatted date. If the current user is the owner of the signature, a small trash icon appears; clicking it calls a Firebase helper that deletes the record by name and updates the local list.</p>
<p>To smooth out the loading state, the page shows a couple of skeleton cards while the initial request to Firebase is in progress. This keeps the layout stable and visually explains that the content is being fetched instead of leaving an empty page.</p>
<p>In the end, this page turned a simple portfolio into a place where visitors can literally leave their mark. It mixes authentication, realtime-like updates and a small drawing tool into something that feels personal and a bit nostalgic.</p>
]]></description>
            <link>https://devollox.fun/blog/signature-page</link>
            <guid isPermaLink="false">https://devollox.fun/blog/signature-page</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[404 as a Playground]]></title>
            <description><![CDATA[<p>A 404 page is usually the most boring place on a site: a couple of lines about “not found” and maybe a sad illustration. For this project, the 404 turned into a small playground instead. If the user landed on a dead link, the page at least had to be visually alive.</p>
<p>The idea was to draw Conway’s Game of Life in the background, full‑screen, behind the usual 404 text. The content stays simple and readable, but the page slowly moves under it, like a quiet simulation that never fully repeats.</p>
<h2>Attaching the canvas to the page</h2>
<p>The Game of Life runs inside a React component that only works with the DOM in a <code>useEffect</code>. It creates a <code>&lt;canvas&gt;</code> element on mount, attaches it to a root container and stretches it to the full viewport.</p>
<pre><code class="language-javascript">import { useEffect } from &#39;react&#39;

export function GameOfLife() {
  useEffect(() =&gt; {
    const root = document.getElementById(&#39;Canvas&#39;)
    if (!root) return

    const canvas = document.createElement(&#39;canvas&#39;)
    const ctx = canvas.getContext(&#39;2d&#39;)
    if (!ctx) return

    canvas.width = window.innerWidth
    canvas.height = window.innerHeight
    canvas.style.position = &#39;fixed&#39;
    canvas.style.top = &#39;0&#39;
    canvas.style.left = &#39;0&#39;
    canvas.style.border = &#39;none&#39;
    canvas.style.zIndex = &#39;0&#39;

    const bg =
      getComputedStyle(document.documentElement).getPropertyValue(
        &#39;--background&#39;
      ) || &#39;#000&#39;

    canvas.style.backgroundColor = bg.trim()

    // ...
  }, [])

  return null
}
</code></pre>
<p>The background color is read from a CSS variable (<code>--background</code>), so the 404 page automatically follows the current theme without extra props or config.</p>
<p>On the 404 page itself, the only requirement is to render a container with <code>id=&quot;Canvas&quot;</code> and include <code>&lt;GameOfLife /&gt;</code> once.</p>
<pre><code class="language-javascript">const NotFoundPage = () =&gt; {
  return (
    &lt;Page title=&quot;404&quot; description=&quot;Page not found.&quot;&gt;
      &lt;div id=&quot;Canvas&quot; /&gt;
      &lt;ContentBlock /&gt;
      &lt;GameOfLife /&gt;
    &lt;/Page&gt;
  )
}
</code></pre>
<p>The text and links are rendered above the canvas using normal layout and a higher z‑index, so the background stays purely decorative.</p>
<h2>Implementing Conway&#39;s Game of Life</h2>
<p>The simulation follows the classic rules of Conway’s Game of Life: a grid of cells, each either alive or dead, evolving based on the number of alive neighbours. The grid is mapped to the canvas as small rectangles.</p>
<pre><code class="language-javascript">const cellSize = 10
const widthCells = Math.ceil(canvas.width / cellSize)
const heightCells = Math.ceil(canvas.height / cellSize)

let firstGeneration: number[][] = Array.from({ length: widthCells }, () =&gt;
  Array.from({ length: heightCells }, () =&gt; (Math.random() * 100 &gt; 80 ? 1 : 0))
)

const getCellValue = (x: number, y: number) =&gt; {
  if (x &lt; 0) x = widthCells - 1
  if (x &gt;= widthCells) x = 0
  if (y &lt; 0) y = heightCells - 1
  if (y &gt;= heightCells) y = 0
  return firstGeneration[x][y]
}
</code></pre>
<p>The grid wraps around the edges: if the cell index goes out of bounds, it continues from the opposite side. This creates a seamless, toroidal world without hard borders.</p>
<p>The next generation is computed on a fixed interval. Each cell counts its eight neighbours; new cells are born when they have exactly three neighbours, and live cells die from overpopulation or isolation.</p>
<pre><code class="language-javascript">const createNewGeneration = () =&gt; {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  const newGeneration: number[][] = Array.from({ length: widthCells }, () =&gt;
    Array(heightCells).fill(0)
  )

  for (let x = 0; x &lt; widthCells; x++) {
    for (let y = 0; y &lt; heightCells; y++) {
      const lifePower =
        getCellValue(x - 1, y - 1) +
        getCellValue(x - 1, y) +
        getCellValue(x - 1, y + 1) +
        getCellValue(x, y - 1) +
        getCellValue(x, y + 1) +
        getCellValue(x + 1, y - 1) +
        getCellValue(x + 1, y) +
        getCellValue(x + 1, y + 1)

      if (firstGeneration[x][y] === 0 &amp;&amp; lifePower === 3) {
        newGeneration[x][y] = 1
      } else if (
        firstGeneration[x][y] === 1 &amp;&amp;
        (lifePower &gt; 3 || lifePower &lt; 2)
      ) {
        newGeneration[x][y] = 0
      } else {
        newGeneration[x][y] = firstGeneration[x][y]
      }

      if (newGeneration[x][y]) {
        ctx.fillStyle = &#39;#99999937&#39;
        ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize)
      }
    }
  }

  firstGeneration = newGeneration
}
</code></pre>
<p>The fill color is semi‑transparent, which keeps the overall look subtle and prevents the background from overpowering the text on top.</p>
<p>To avoid the grid falling into a boring stable state forever, the platform is periodically refreshed with a new random seed.</p>
<pre><code class="language-javascript">const refreshPlatform = () =&gt; {
  for (let i = 0; i &lt; widthCells; i++) {
    for (let j = 0; j &lt; heightCells; j++) {
      firstGeneration[i][j] = Math.random() * 100 &gt; 80 ? 1 : 0
    }
  }
}
</code></pre>
<h2>Running and cleaning up the simulation</h2>
<p>Two intervals are started when the component mounts: one to step the simulation, another to refresh the grid from time to time.</p>
<pre><code class="language-javascript">root.appendChild(canvas)

const intervalId = setInterval(createNewGeneration, 100)
const refreshId = setInterval(refreshPlatform, 60000 * 30)

return () =&gt; {
  clearInterval(intervalId)
  clearInterval(refreshId)
  if (canvas.parentNode) {
    canvas.parentNode.removeChild(canvas)
  }
}
</code></pre>
<p>This keeps the Game of Life running smoothly while the user is on the page and ensures that everything is properly cleaned up when navigating away: no orphaned canvas, no leaking intervals.</p>
<p>In the end, the 404 page became a quiet nod to cellular automata: even when the route is wrong, the background is still doing something interesting.</p>
]]></description>
            <link>https://devollox.fun/blog/404-game-of-life</link>
            <guid isPermaLink="false">https://devollox.fun/blog/404-game-of-life</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Mon, 10 Jun 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Encoding Text Effects]]></title>
            <description><![CDATA[<p>When I started building the <code>Uses</code> page, the layout felt a bit too static. I wanted to add a small detail that would make the text feel more alive without turning the whole site into a playground of animations. That turned into a simple encoding effect on headings.</p>
<p>The idea is straightforward: take the final string, replace all characters with random ones from a small alphabet and then gradually reveal the real text from left to right. It creates a short “decoding” animation every time the element mounts.</p>
<h2>The encoding hook</h2>
<p>The whole effect lives in a small React hook that targets an element by <code>id</code> and updates its text content on an interval.</p>
<pre><code class="language-javascript">import { useEffect } from &#39;react&#39;

const useEncoding = (id: string | number, speed: number) =&gt; {
  useEffect(() =&gt; {
    const targetText = document.getElementById(String(id))
    if (!targetText) return

    const targetString = targetText.textContent || &#39;&#39;
    const alphabet = &#39; abcdefghijklmnopqrstuvwxyz&#39;

    const getRandomChar = () =&gt;
      alphabet[Math.floor(Math.random() * alphabet.length)]

    let currentString = Array(targetString.length).fill(&#39;&#39;)
    let index = 0

    const intervalId = setInterval(() =&gt; {
      for (let i = 0; i &lt; currentString.length; i++) {
        if (i &lt; index) {
          currentString[i] = targetString[i]
        } else {
          currentString[i] = getRandomChar()
        }
      }

      targetText.textContent = currentString.join(&#39;&#39;)

      if (index &gt;= targetString.length) {
        clearInterval(intervalId)
      } else {
        index++
      }
    }, speed)

    return () =&gt; clearInterval(intervalId)
  }, [id, speed])
}

export default useEncoding
</code></pre>
<p>The <code>alphabet</code> can be tuned for different vibes: only letters for a softer look, symbols or numbers for something closer to a “terminal” feeling.</p>
<h2>Using the effect on headings</h2>
<p>On the page side, the hook is attached to any element by giving it a stable <code>id</code>. As soon as the component mounts, the text starts “encoding” and then resolves to the final value.</p>
<pre><code class="language-javascript">const EncodedTitle = () =&gt; {
  const id = &#39;computer-heading&#39;

  useEncoding(id, 40)

  return &lt;h1 id={id}&gt;Computer&lt;/h1&gt;
}
</code></pre>
<p>The same pattern can be reused for section titles, hero slogans or small labels. A lower <code>speed</code> value makes the animation faster, a higher one slows it down and makes each step more visible.</p>
<h2>Where it is used on the site</h2>
<p>This hook is wired into several headings and labels across the site, mostly on pages that are already about hardware and tools. It adds a small layer of motion to an otherwise static layout and keeps the interaction subtle enough to not distract from the content.</p>
<p>The next step for this effect is to make it run asynchronously and maybe trigger on scroll or hover instead of only on mount. But even in this simple version it already does exactly what it should: gives the site a tiny bit of that “encoding” feel without getting in the way.</p>
]]></description>
            <link>https://devollox.fun/blog/encodingtext-effect</link>
            <guid isPermaLink="false">https://devollox.fun/blog/encodingtext-effect</guid>
            <dc:creator><![CDATA[Devollox]]></dc:creator>
            <pubDate>Tue, 28 May 2024 00:00:00 GMT</pubDate>
        </item>
    </channel>
</rss>