<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ivan kulkin</title>
    <description>The latest articles on DEV Community by ivan kulkin (@ivan_kulkin_1522025957eee).</description>
    <link>https://dev.to/ivan_kulkin_1522025957eee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4068094%2F2d1b3752-35a2-43b5-b932-d072f98dc8e7.png</url>
      <title>DEV Community: ivan kulkin</title>
      <link>https://dev.to/ivan_kulkin_1522025957eee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ivan_kulkin_1522025957eee"/>
    <language>en</language>
    <item>
      <title>I added AI to OKLCH Pixel Palette.</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Sat, 22 Aug 2026 17:21:11 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/i-added-ai-to-oklch-pixel-palette-1ia0</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/i-added-ai-to-oklch-pixel-palette-1ia0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fficxgi8f6t6ork7sln9w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fficxgi8f6t6ork7sln9w.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added AI palette generation to OKLCH Pixel Palette.&lt;/p&gt;

&lt;p&gt;The idea is pretty simple: instead of opening Lospec, Pinterest or another palette collection and scrolling until something feels close enough, you can describe the atmosphere you need in normal words.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;зимний лес&lt;/code&gt;&lt;br&gt;
&lt;code&gt;бледно-розовый рассвет&lt;/code&gt;&lt;br&gt;
&lt;code&gt;фиолетовая пещера&lt;/code&gt;&lt;br&gt;
&lt;code&gt;deep sea horror&lt;/code&gt;&lt;br&gt;
&lt;code&gt;rusty factory at sunset&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The site turns the description into a palette that can immediately be used for pixel art, illustrations, UI, landing pages or website design.&lt;/p&gt;

&lt;p&gt;This is useful when you know the mood you want, but don't know the exact colors yet. An artist can start with a scene or atmosphere. A web designer can describe the visual direction of a page and get a base palette instead of manually trying dozens of combinations.&lt;/p&gt;

&lt;p&gt;I didn't want this feature to be an API wrapper. The model runs directly in the browser. There are no OpenAI, Gemini or other AI API requests involved in generation. After the model files are loaded, palette generation works locally on your device and doesn't require an internet connection.&lt;/p&gt;

&lt;p&gt;The existing OKLCH color engine is still responsible for building the final palette, keeping colors inside the sRGB gamut and generating usable combinations. AI only interprets the text and chooses the visual direction.&lt;/p&gt;

&lt;p&gt;One of the reasons this update took longer than expected was a pretty funny failure in the first version: I typed &lt;code&gt;black&lt;/code&gt; and got brown.&lt;/p&gt;

&lt;p&gt;The model itself understood the text, but the small layer translating language embeddings into colors generalized badly. I ended up replacing that part with semantic color anchors and a separate constraint for explicit color names.&lt;/p&gt;

&lt;p&gt;Now the same system can handle both obvious requests such as &lt;code&gt;black&lt;/code&gt; or &lt;code&gt;фиолетовый&lt;/code&gt; and more descriptive ones such as &lt;code&gt;заброшенная больница ночью&lt;/code&gt; or &lt;code&gt;неоновый киберпанк под дождём&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It supports Russian and English, runs locally, and the normal manual palette tools are still available if you don't want AI at all.&lt;/p&gt;

&lt;p&gt;Try it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://oklchpalette.ru" rel="noopener noreferrer"&gt;https://oklchpalette.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>offline</category>
    </item>
    <item>
      <title>How I made uncertainty readable in a deterministic browser strategy</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:16:16 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/how-i-made-uncertainty-readable-in-a-deterministic-browser-strategy-ld6</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/how-i-made-uncertainty-readable-in-a-deterministic-browser-strategy-ld6</guid>
      <description>&lt;p&gt;I wanted to build a strategy game where tension appears &lt;strong&gt;before&lt;/strong&gt; the click. Random outcomes are easy to add, but they often feel arbitrary. MYCELIUM became an experiment in exposing enough information for a risky move to feel like a decision rather than a coin flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The board shows uncertainty instead of hiding it
&lt;/h2&gt;

&lt;p&gt;The world is generated from a deterministic seed. A frontier cell is still unknown, but its forecast comes from the same seeded biome that will resolve when the player reveals it. The UI is not inventing flavor text after the fact.&lt;/p&gt;

&lt;p&gt;Before an attack, the player sees the final probability and the local support around the target. Eight neighboring cells matter, so territory shape affects the odds directly.&lt;/p&gt;

&lt;p&gt;That creates a useful distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the result can remain uncertain;&lt;/li&gt;
&lt;li&gt;the inputs are visible;&lt;/li&gt;
&lt;li&gt;replaying the same seed remains reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enemy plans are objects on the board
&lt;/h2&gt;

&lt;p&gt;Hostile growth is announced before it resolves. An intent stores its source, target, direction, and chance. The player gets one turn to respond.&lt;/p&gt;

&lt;p&gt;This matters because the counterplay is spatial. You can reinforce the target, spend a rare guaranteed Repaint, or capture the source and remove the threat entirely. The intent is not a timer floating above the UI; it belongs to a living tendril on the board.&lt;/p&gt;

&lt;h2&gt;
  
  
  One geometric rule creates chains
&lt;/h2&gt;

&lt;p&gt;The main territory rule is simple: close a same-family square perimeter of at least 3×3 and its interior becomes captured and reinforced.&lt;/p&gt;

&lt;p&gt;The interesting part is recursion. Filling one interior can complete a second square, so a single corner may trigger a chain of captures. The implementation needs to keep searching for newly completed shapes without processing the same square forever.&lt;/p&gt;

&lt;p&gt;The result is a board where a locally weak move can be globally correct because it completes future geometry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the game static
&lt;/h2&gt;

&lt;p&gt;The stack is Next.js 16, React 19, TypeScript, and PixiJS. Solo play runs entirely in the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic world generation;&lt;/li&gt;
&lt;li&gt;local saves and records;&lt;/li&gt;
&lt;li&gt;daily seeds and challenge links;&lt;/li&gt;
&lt;li&gt;no account, database, or API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optional 1v1 uses direct P2P rooms. The static deployment remains enough for the game itself; TURN configuration is only a connectivity fallback for restrictive networks.&lt;/p&gt;

&lt;p&gt;The public site also contains a server-rendered lore atlas. This gives the fictional world a crawlable surface instead of hiding every piece of writing inside a canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am testing now
&lt;/h2&gt;

&lt;p&gt;The hardest question is not technical: does showing forecasts, attack support, and enemy intents produce better decisions, or just more information to parse?&lt;/p&gt;

&lt;p&gt;I would value focused feedback on the first match:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When did you first understand why a square matters?&lt;/li&gt;
&lt;li&gt;Did an announced enemy intent change your plan?&lt;/li&gt;
&lt;li&gt;Which number or preview felt unnecessary?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Play MYCELIUM:&lt;/strong&gt; &lt;a href="https://mycelum.vercel.app/" rel="noopener noreferrer"&gt;https://mycelum.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/vansGAMee/MYCELUM" rel="noopener noreferrer"&gt;https://github.com/vansGAMee/MYCELUM&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

</description>
      <category>opensource</category>
      <category>gamedev</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why OKLCH CSS variables need @supports for a real fallback</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:49:04 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/why-oklch-css-variables-need-supports-for-a-real-fallback-2fgp</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/why-oklch-css-variables-need-supports-for-a-real-fallback-2fgp</guid>
      <description>&lt;p&gt;I added CSS export to an OKLCH palette generator this week. The obvious output looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5b21b6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;43.22%&lt;/span&gt; &lt;span class="m"&gt;0.196&lt;/span&gt; &lt;span class="m"&gt;293.54&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like ordinary progressive enhancement: older browsers ignore the second declaration and keep the HEX value. That assumption is unreliable for custom properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure happens when the variable is used
&lt;/h2&gt;

&lt;p&gt;A custom property accepts almost any token sequence. A browser can preserve &lt;code&gt;oklch(...)&lt;/code&gt; as the value of &lt;code&gt;--brand&lt;/code&gt; even if it cannot use that function as a color. The second declaration therefore replaces the first one.&lt;/p&gt;

&lt;p&gt;The browser only discovers the unsupported color when the variable reaches a typed property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--brand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point the declaration can become invalid. The HEX line did not survive as a fallback because both declarations set the same untyped custom property.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the override behind a feature query
&lt;/h2&gt;

&lt;p&gt;The exported file now separates the universal values from the modern override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--palette-1-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#160067&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--palette-2-base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5b21b6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--palette-3-highlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ab7ce1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--palette-4-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4dcd49&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--palette-1-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;25.12%&lt;/span&gt; &lt;span class="m"&gt;0.151&lt;/span&gt; &lt;span class="m"&gt;280.34&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--palette-2-base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;43.22%&lt;/span&gt; &lt;span class="m"&gt;0.196&lt;/span&gt; &lt;span class="m"&gt;293.54&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--palette-3-highlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;67.4%&lt;/span&gt; &lt;span class="m"&gt;0.118&lt;/span&gt; &lt;span class="m"&gt;301.12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--palette-4-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;74.82%&lt;/span&gt; &lt;span class="m"&gt;0.188&lt;/span&gt; &lt;span class="m"&gt;142.28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browsers without OKLCH support keep the sRGB values. Browsers that pass the feature query receive the perceptual values. Consumers do not need to know which path was selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--palette-3-highlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--palette-2-base&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--palette-4-accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The variable names include both order and role. The numeric prefix keeps them unique when a larger generated palette contains two colors with similar human-readable names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why export both formats?
&lt;/h2&gt;

&lt;p&gt;OKLCH is useful when a palette needs controlled lightness steps. That matters in UI states and in small sprites where two colors with different RGB values can still look almost identical. HEX remains the practical compatibility layer and accurately represents the generator's sRGB-fitted output.&lt;/p&gt;

&lt;p&gt;The generator handles the rest of the workflow in the browser: 2–9 colors, six harmony modes, sRGB gamut fitting, lightness inspection, sprite previews, and exports for Aseprite PAL, GIMP GPL, HEX, JSON, TXT, PNG, and now CSS. It requires no account.&lt;/p&gt;

&lt;p&gt;Try the CSS export: &lt;a href="https://oklchpalette.ru/create" rel="noopener noreferrer"&gt;https://oklchpalette.ru/create&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/vansGAMee/OKLCH-PIXEL-PALETTE" rel="noopener noreferrer"&gt;https://github.com/vansGAMee/OKLCH-PIXEL-PALETTE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next candidates are Tailwind theme output, a Sass map, and a Godot resource. I would be interested to know which one would remove the most manual work from your workflow.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>design</category>
    </item>
    <item>
      <title>Building an OKLCH palette generator that survives gamut limits and exports to Aseprite</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:04:10 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/building-an-oklch-palette-generator-that-survives-gamut-limits-and-exports-to-aseprite-4g51</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/building-an-oklch-palette-generator-that-survives-gamut-limits-and-exports-to-aseprite-4g51</guid>
      <description>&lt;p&gt;A row of attractive swatches is not yet a useful pixel-art palette.&lt;/p&gt;

&lt;p&gt;A working palette needs a readable value structure, controlled hue relationships, predictable ramps, colors that actually fit inside sRGB, and an export format the artist can use without rebuilding everything by hand.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://oklchpalette.ru" rel="noopener noreferrer"&gt;OKLCH Pixel Palette&lt;/a&gt; around that workflow. It is free, open source, requires no account, and runs entirely in the browser.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/vansGAMee/OKLCH-PIXEL-PALETTE" rel="noopener noreferrer"&gt;github.com/vansGAMee/OKLCH-PIXEL-PALETTE&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OKLCH instead of HSL
&lt;/h2&gt;

&lt;p&gt;In HSL, two colors with the same numerical lightness can look very different in brightness. That is inconvenient in UI work and especially obvious in small sprites, where one bad value step can flatten the whole form.&lt;/p&gt;

&lt;p&gt;OKLCH separates perceptual lightness (&lt;code&gt;L&lt;/code&gt;), chroma (&lt;code&gt;C&lt;/code&gt;) and hue (&lt;code&gt;H&lt;/code&gt;). It does not make color decisions automatic, but it gives the generator a more useful coordinate system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lightness steps can be planned as a visible value ladder;&lt;/li&gt;
&lt;li&gt;chroma can be reduced without unintentionally steering the hue;&lt;/li&gt;
&lt;li&gt;harmony rules can operate on hue while the value structure remains inspectable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool generates between 2 and 9 colors from a base color and supports six harmony modes. The important part is not the number of modes. It is that every result remains editable and visible as a lightness progression rather than arriving as a sealed random answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gamut mapping is product behavior, not cleanup
&lt;/h2&gt;

&lt;p&gt;Browsers can describe OKLCH colors outside the sRGB gamut. Many pixel-art pipelines cannot store or display those colors consistently.&lt;/p&gt;

&lt;p&gt;Simply clipping red, green and blue channels produces unpleasant shifts near the boundary. The generator instead reduces chroma until the color fits the target gamut while trying to preserve the intended lightness and hue. It then checks the converted result rather than assuming the math behaved.&lt;/p&gt;

&lt;p&gt;Near-black and near-white seeds are good tests because they expose fragile conversions quickly. A palette tool should not turn &lt;code&gt;#010101&lt;/code&gt; into a colorful surprise or create several visually identical highlight steps. Those edge cases influenced the lightness limits, chroma search and duplicate handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swatches are not enough
&lt;/h2&gt;

&lt;p&gt;Palette quality depends on use. A ramp that looks elegant in a horizontal strip can fail as soon as it must describe a one-pixel edge.&lt;/p&gt;

&lt;p&gt;The site therefore shows live pixel previews—a potion, gem, shield and character—next to the palette. They are intentionally small and familiar. Their job is to reveal value collisions, weak outlines and over-bright accents before export.&lt;/p&gt;

&lt;p&gt;The same thinking shaped the editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a visible lightness ladder instead of hidden coefficients;&lt;/li&gt;
&lt;li&gt;Delta E checks for colors that are technically different but perceptually too close;&lt;/li&gt;
&lt;li&gt;an sRGB gamut guard;&lt;/li&gt;
&lt;li&gt;direct editing after generation;&lt;/li&gt;
&lt;li&gt;no account wall between experiment and export.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Export should end the task
&lt;/h2&gt;

&lt;p&gt;Copying HEX values one at a time is not an export workflow. OKLCH Pixel Palette can produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JASC PAL for Aseprite;&lt;/li&gt;
&lt;li&gt;GIMP GPL;&lt;/li&gt;
&lt;li&gt;HEX and JSON;&lt;/li&gt;
&lt;li&gt;a PNG palette strip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the generated palette a usable asset rather than a screenshot of an idea.&lt;/p&gt;

&lt;p&gt;I previously experimented with accounts and cloud saving. I removed both. Generation, editing, import and export now stay in the browser. The product is simpler, there is no personal palette database to maintain, and opening the site is enough to start work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to test it
&lt;/h2&gt;

&lt;p&gt;Try &lt;a href="https://oklchpalette.ru/tools/pixel-art-palette-generator" rel="noopener noreferrer"&gt;the pixel-art palette generator&lt;/a&gt; with three deliberately difficult seeds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a nearly black neutral;&lt;/li&gt;
&lt;li&gt;a very bright saturated blue;&lt;/li&gt;
&lt;li&gt;a warm low-chroma midtone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Change the palette size, compare harmony modes, watch the sprite previews, then export the result as JASC PAL and open it in Aseprite.&lt;/p&gt;

&lt;p&gt;If the ramp needs manual correction, I want to know where and why. A useful palette tool should make the correction obvious instead of hiding behind the word “smart.”&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://oklchpalette.ru" rel="noopener noreferrer"&gt;oklchpalette.ru&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/vansGAMee/OKLCH-PIXEL-PALETTE" rel="noopener noreferrer"&gt;github.com/vansGAMee/OKLCH-PIXEL-PALETTE&lt;/a&gt;``&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>opensource</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I built a browser chess client that can play Lichess games without becoming another chess server</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:03:16 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/i-built-a-browser-chess-client-that-can-play-lichess-games-without-becoming-another-chess-server-2k2i</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/i-built-a-browser-chess-client-that-can-play-lichess-games-without-becoming-another-chess-server-2k2i</guid>
      <description>&lt;p&gt;I wanted a chess board that felt precise, opened without an account, and told me something useful after the game. I did not want to build another closed player pool and then pretend that an empty matchmaking queue was a feature.&lt;/p&gt;

&lt;p&gt;That became &lt;a href="https://veyrn-chess.vercel.app" rel="noopener noreferrer"&gt;VEYRN Chess&lt;/a&gt;: a free, open-source chess client with two ways to play online.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send a private-room link and play directly browser to browser over WebRTC.&lt;/li&gt;
&lt;li&gt;Connect a Lichess account and find a casual 10+0 opponent through the official Lichess Board API while staying on the VEYRN board.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source is on &lt;a href="https://github.com/vansGAMee/veyrn_chess" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The empty-network problem
&lt;/h2&gt;

&lt;p&gt;A new multiplayer product starts with a cruel loop: no players means slow matchmaking, and slow matchmaking means no players.&lt;/p&gt;

&lt;p&gt;Private rooms solve half of it. The host creates a link, the guest opens it, a short-lived serverless signaling route exchanges WebRTC offers and ICE candidates, and the game then moves to an encrypted DataChannel. The signaling data expires after a few minutes; it is not a game archive.&lt;/p&gt;

&lt;p&gt;But a private link is useless when your friends are offline. For that case VEYRN can act as a Lichess client. OAuth uses PKCE in the browser, VEYRN opens the official Board API event stream, starts a casual rapid seek, and sends legal moves back to Lichess. Lichess remains the server and source of truth; VEYRN is only the interface.&lt;/p&gt;

&lt;p&gt;This is deliberately not bot scraping and not an unofficial bridge. It uses the API Lichess provides for third-party boards and clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The board had to earn its place
&lt;/h2&gt;

&lt;p&gt;Most chess interfaces are usable. The harder target is making the board disappear under your hand.&lt;/p&gt;

&lt;p&gt;VEYRN uses pointer capture and transform-based dragging so a piece stays locked to the pointer instead of jumping toward whichever edge was grabbed. Click-to-move and drag-to-move share the same legal-move path. Premoves are shown as a separate visual state and are revalidated when the turn changes. Right-click arrows and square markers live on a non-blocking SVG layer, so planning marks never steal a move.&lt;/p&gt;

&lt;p&gt;The visual problem was just as concrete. Coordinates, legal-move dots, last-move squares, check state, pieces and arrows all compete for contrast. I ended up treating the board as an instrument panel: restrained mineral squares, a high-contrast piece set, crisp coordinates, and state colors that remain distinct from both square colors.&lt;/p&gt;

&lt;p&gt;On mobile, time controls are full-size targets rather than tiny inline links. The end-of-game report sits over the board and gives the useful actions immediately: next opponent, private room, PGN, or the behavioral report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics that stay on the device
&lt;/h2&gt;

&lt;p&gt;VEYRN records completed games in local browser storage. The report goes beyond wins and losses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;think time by opening, middlegame and endgame;&lt;/li&gt;
&lt;li&gt;clock pressure and time-spending distribution;&lt;/li&gt;
&lt;li&gt;move timing and decision rhythm;&lt;/li&gt;
&lt;li&gt;captures, checks, castling and move-pattern summaries;&lt;/li&gt;
&lt;li&gt;network mode and country matchup context;&lt;/li&gt;
&lt;li&gt;finished PGN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no VEYRN profile and no cloud game history. Deleting browser data deletes the ledger. The site uses anonymous, cookie-free Vercel page-view analytics, but moves, PGNs and local reports are not sent with those events.&lt;/p&gt;

&lt;p&gt;Country flags are detected approximately from the connection and can be overridden manually. In a private room each side receives the other player's selected country code, so two players do not collapse into one default flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was unexpectedly difficult
&lt;/h2&gt;

&lt;p&gt;Three parts took more time than the headline features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serverless signaling latency.&lt;/strong&gt; A serverless instance cannot be treated like a permanent WebSocket process. Messages need short polling, deduplication, expiry and recovery without making room creation feel stalled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authoritative remote state.&lt;/strong&gt; A Lichess stream may reconnect or deliver state after the local animation. The client must reconcile clocks, move history and results without accepting an optimistic illegal move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input geometry.&lt;/strong&gt; A board that looks correct can still feel wrong. Pointer coordinates, board orientation, transforms and capture all have to agree at every viewport size.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try to break it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://veyrn-chess.vercel.app" rel="noopener noreferrer"&gt;VEYRN Chess&lt;/a&gt;, choose a time control, and either create a private room or use &lt;strong&gt;Play on Lichess&lt;/strong&gt;. No VEYRN registration is required.&lt;/p&gt;

&lt;p&gt;I am especially interested in blunt feedback about three things: how the first piece pickup feels, whether board states remain readable on your display, and whether the local report reveals anything you would actually use before the next game.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/vansGAMee/veyrn_chess" rel="noopener noreferrer"&gt;github.com/vansGAMee/veyrn_chess&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built an OKLCH palette generator for pixel art and UI</title>
      <dc:creator>ivan kulkin</dc:creator>
      <pubDate>Fri, 07 Aug 2026 21:25:47 +0000</pubDate>
      <link>https://dev.to/ivan_kulkin_1522025957eee/i-built-an-oklch-palette-generator-for-pixel-art-and-ui-27id</link>
      <guid>https://dev.to/ivan_kulkin_1522025957eee/i-built-an-oklch-palette-generator-for-pixel-art-and-ui-27id</guid>
      <description>&lt;p&gt;Hi! I built OKLCH Pixel Palette, a free and open-source browser tool for generating color palettes with OKLCH.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;p&gt;generate 2–9 color palettes&lt;br&gt;
preview colors on pixel art&lt;br&gt;
copy HEX values&lt;br&gt;
export palettes as PNG, GPL and PAL&lt;br&gt;
save your palettes to the cloud&lt;br&gt;
publish palettes to the public gallery&lt;br&gt;
browse and save palettes made by other users&lt;br&gt;
share palettes with a link&lt;/p&gt;

&lt;p&gt;The project is open source, so you can also check out the code and contribute.&lt;/p&gt;

&lt;p&gt;I built it because I wanted a fast way to work with perceptually consistent colors without manually tweaking everything.&lt;/p&gt;

&lt;p&gt;I’d love feedback, especially on the palette generation and UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://oklchpalette.ru" rel="noopener noreferrer"&gt;oklchpalette.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>css</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
