<?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: 김기선</title>
    <description>The latest articles on DEV Community by 김기선 (@_f2ad1d4a811a87449655b).</description>
    <link>https://dev.to/_f2ad1d4a811a87449655b</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%2F4220035%2F21bbe0a8-cf5f-4100-a4b7-7e8cb0b671e9.png</url>
      <title>DEV Community: 김기선</title>
      <link>https://dev.to/_f2ad1d4a811a87449655b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_f2ad1d4a811a87449655b"/>
    <language>en</language>
    <item>
      <title>Why brightness and contrast order mattered: correcting a browser editor’s pixel response</title>
      <dc:creator>김기선</dc:creator>
      <pubDate>Sun, 01 Nov 2026 07:22:41 +0000</pubDate>
      <link>https://dev.to/_f2ad1d4a811a87449655b/why-brightness-and-contrast-order-mattered-correcting-a-browser-editors-pixel-response-492h</link>
      <guid>https://dev.to/_f2ad1d4a811a87449655b/why-brightness-and-contrast-order-mattered-correcting-a-browser-editors-pixel-response-492h</guid>
      <description>&lt;p&gt;A brightness slider can work perfectly as UI while producing the wrong pixels. In Photoshot, my browser image editor, the difficult part was matching a measured brightness/contrast response while keeping the adjustment non-destructive and integrated with masks, opacity and saved projects.&lt;/p&gt;

&lt;p&gt;This is a compact account of the reference code, the numerical failure, the correction, and what was actually verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The first implementation and its reference
&lt;/h2&gt;

&lt;p&gt;The September 18 development record shows Konva's &lt;code&gt;src/filters/Contrast.ts&lt;/code&gt; being inspected before the first adjustment engine was written. Its midpoint-centered pixel loop was useful for understanding the channel range and the fact that contrast need not modify alpha.&lt;/p&gt;

&lt;p&gt;Photoshot used separately written expressions: brighten toward white or darken toward black, then apply contrast around normalized midpoint 0.5. The original-revision branch remains in the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brightness&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&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 engine already compiled three 256-entry channel lookup tables. The later correction changed their response calculation, not the existence of lookup tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The measured failure
&lt;/h2&gt;

&lt;p&gt;On September 25, an intermediate &lt;strong&gt;normal-mode approximation&lt;/strong&gt; was compared with Photoshop 27.3.1 at brightness +30 and contrast +20. It produced mean absolute RGB-channel error &lt;strong&gt;11.505&lt;/strong&gt; and maximum error &lt;strong&gt;25&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two retained gray samples make the failure concrete:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Intermediate output&lt;/th&gt;
&lt;th&gt;Archived Photoshop output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;157&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those errors belong to the intermediate build used in that comparison. They must not be presented as a measurement of the first September 18 legacy formula.&lt;/p&gt;

&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%2Fqjcl6vwlnsizqhj3l985.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%2Fqjcl6vwlnsizqhj3l985.png" alt="Archived mean and maximum errors before correction and with both response orders" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Historical chart measurements at +30/+20; each bar is one deterministic evaluation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why consulting GitHub was not the entire solution
&lt;/h2&gt;

&lt;p&gt;Konva was a consulted source, not an installed, verbatim replacement for our filter. A public contrast implementation may target a different response curve. It also does not supply our adjustment-layer serialization, mask integration or old-project behavior.&lt;/p&gt;

&lt;p&gt;The actual chronology was reference lookup → application implementation → later comparison → calibration. The reference was not first discovered after the September 25 failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Replace the approximation with sampled responses
&lt;/h2&gt;

&lt;p&gt;Calibration used a synthetic gray ramp in Photoshop normal mode. Brightness was sampled from −150 to +150 and contrast from −50 to +100, in steps of 10, with the other control held at zero. The data are retained in &lt;code&gt;tone-response.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sampleResponse&lt;/code&gt; interpolates intermediate settings and channel positions. Revision 2 normal mode applies brightness first, then contrast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;            &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toneResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toneResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is meaningful: &lt;strong&gt;C(B(x)) generally differs from B(C(x))&lt;/strong&gt;. The result still flows through the existing floating-point channel tables. Masks and opacity remain compositor responsibilities; old revision behavior and the separate Legacy option remain distinct branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The result—and a control that reverses the order
&lt;/h2&gt;

&lt;p&gt;The historical chart was 256 × 32 pixels: eight gray-ramp rows followed by mixed RGB rows. Alpha was excluded, leaving 24,576 channel samples.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Historical implementation, +30/+20&lt;/th&gt;
&lt;th&gt;Mean absolute error&lt;/th&gt;
&lt;th&gt;Maximum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate approximation&lt;/td&gt;
&lt;td&gt;11.505&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measured response, brightness → contrast&lt;/td&gt;
&lt;td&gt;0.241211&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measured response, contrast → brightness&lt;/td&gt;
&lt;td&gt;1.149740&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For that chart condition, the mean error fell by approximately &lt;strong&gt;97.9%&lt;/strong&gt; relative to the intermediate approximation. That is an error reduction, not a universal compatibility or visual-quality score. Each row is one deterministic chart evaluation, so there is no invented confidence interval or speed claim.&lt;/p&gt;

&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%2Fmdmczmwcs1v9yl9jzjka.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%2Fmdmczmwcs1v9yl9jzjka.png" alt="Archived failed gray values, Photoshop comparison points and the current source replay" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Numeric records visualized as patches and curves; historical measurements and the new replay remain distinct.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What the current rerun verifies
&lt;/h2&gt;

&lt;p&gt;The unchanged public engine's response tables were compared with the archived calibration JSON and match exactly. A fresh source-level replay then evaluated nine retained gray inputs at each of +30/+20, +17/+13 and −65/−23: &lt;strong&gt;27/27 outputs equal the archived Photoshot outputs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That does not mean 27/27 equal Photoshop exactly; some retained Photoshop comparisons differ by one channel value. Nor does this rerun recompute the full-chart means: the original reference PNGs were unavailable in the reviewed folder. Historical chart statistics, archived Photoshop samples and the new replay are deliberately separated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and full figures
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/konvajs/konva/blob/master/src/filters/Contrast.ts" rel="noopener noreferrer"&gt;Konva Contrast.ts: consulted reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Abraham721/photoshot/blob/19f45ed6dd99800f910c0587baa53e73d46ab4fb/src/adjustments.ts" rel="noopener noreferrer"&gt;Photoshot adjustment engine at 19f45ed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Abraham721/photoshot/blob/19f45ed6dd99800f910c0587baa53e73d46ab4fb/src/tone-response.ts" rel="noopener noreferrer"&gt;Measured response data and interpolation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full version, including the pipeline diagram, before/after error charts, gray-value comparisons and reproducibility files, read the &lt;a href="https://slohero.com/photoshot/guides/brightness-contrast/en/" rel="noopener noreferrer"&gt;complete research note on my website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Writing and source review were assisted by AI. Claims above are tied to the cited source and retained measurement records.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Building reversible layer masks in a browser image editor</title>
      <dc:creator>김기선</dc:creator>
      <pubDate>Sun, 01 Nov 2026 07:13:49 +0000</pubDate>
      <link>https://dev.to/_f2ad1d4a811a87449655b/building-reversible-layer-masks-in-a-browser-image-editor-4mjk</link>
      <guid>https://dev.to/_f2ad1d4a811a87449655b/building-reversible-layer-masks-in-a-browser-image-editor-4mjk</guid>
      <description>&lt;p&gt;The feature I wanted was a reversible layer mask: hide part of an image without destroying its source, disable the mask to recover the image, and retain the same document state when it is serialized and composited again. This is a concise engineering account of the implementation in Photoshot, a browser image editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with the document contract
&lt;/h2&gt;

&lt;p&gt;A layer needs its own position, dimensions, rotation, opacity and blend mode. A mask belongs to the editable document, separately from its source image or paint strokes. A rendered display object is a temporary interpretation of that document.&lt;/p&gt;

&lt;p&gt;The first public Stage used React and PixiJS. Its adapter created a Sprite and transferred the document fields into renderer properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blendMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The half-width/height offset follows the centered anchor. Rotation is converted from degrees to radians. These are application-specific integration decisions; installing PixiJS does not supply them.&lt;/p&gt;

&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%2F4tj4uihlrev3w5k6grxh.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%2F4tj4uihlrev3w5k6grxh.png" alt="Document state, temporary surface and shared compositor" width="799" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source-level architecture; arrows show data flow, not timing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where the implementation became difficult
&lt;/h2&gt;

&lt;p&gt;A Sprite does not define our project schema, mask history or export semantics. The early preview used PixiJS while export had a separate Canvas composition loop. That split created two places that needed to interpret the same document correctly.&lt;/p&gt;

&lt;p&gt;Source inspection establishes the separate paths. It does &lt;strong&gt;not&lt;/strong&gt; establish an archived preview/export mismatch for every image. The problem addressed here is the responsibility boundary between persistent state and rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What came from GitHub—and what was written locally
&lt;/h2&gt;

&lt;p&gt;React supplied UI infrastructure. PixiJS supplied the early rendering APIs. The document model, coordinate adapter, mask plumbing and later common compositor were Photoshot code. This was library reuse plus application integration, rather than a transplanted third-party editor.&lt;/p&gt;

&lt;p&gt;Photopea's mask documentation was a behavioral reference: disabling a mask should recover content. It was not a source-code transplant. The concrete implementation references are the initial Photoshot commit &lt;code&gt;eead3ca&lt;/code&gt; and the later &lt;code&gt;19f45ed&lt;/code&gt; (&lt;code&gt;v0.16.0&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Preserve the source; mask a temporary surface
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;layerSurface&lt;/code&gt; draws the retained source into a fresh Canvas and applies the mask there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;destination-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;maskSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-over&lt;/span&gt;&lt;span class="dl"&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;&lt;code&gt;destination-in&lt;/code&gt; retains destination pixels according to mask alpha. The code then restores &lt;code&gt;source-over&lt;/code&gt;. Source strokes and mask strokes remain separate document fields, so disabling the mask regenerates the surface from the source.&lt;/p&gt;

&lt;p&gt;The later Stage also calls the shared &lt;code&gt;composite&lt;/code&gt; function for preview. That consolidates layer order, transforms, blending and adjustments. Preview still bounds the longest edge to 1,600 pixels, so a preview and a full-resolution export need a resolution-matched comparison before claiming pixel equivalence.&lt;/p&gt;

&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%2Foz4o3mlkx8i1lk4tb151.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%2Foz4o3mlkx8i1lk4tb151.png" alt="Source, enabled mask, destructive control and recovered document mask" width="799" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;New diagnostic outputs. The destructive control is not a historical product screenshot.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A controlled failure and a successful recovery
&lt;/h2&gt;

&lt;p&gt;For this article, the unchanged document engine was run with Node and native Canvas on a 100 × 100 pixel paint layer. A 40-pixel mask stroke hid the center. Five deterministic checks passed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source document before/after rendering&lt;/td&gt;
&lt;td&gt;Identical JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enabled mask at the center&lt;/td&gt;
&lt;td&gt;Alpha 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restoring a smaller mask stroke&lt;/td&gt;
&lt;td&gt;Center alpha 255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disabling the document mask&lt;/td&gt;
&lt;td&gt;Entire RGBA array equals the source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON serialize → validate → composite&lt;/td&gt;
&lt;td&gt;Equals the masked render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The negative control erased pixels directly from a retained Canvas. Stopping that erase operation left the center transparent, while disabling the document mask restored it to alpha 255. This control was constructed for the diagnostic; it is &lt;strong&gt;not&lt;/strong&gt; a recovered screenshot of an old Photoshot failure.&lt;/p&gt;

&lt;p&gt;The result supports source preservation for this input and revision. It is not a browser interaction test, an IndexedDB durability test, or a claim that every blend mode and format matches Photoshop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and the complete research note
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Abraham721/photoshot/blob/eead3caec5407c14cdd8cdb698a12c633295f8ef/src/stage.tsx" rel="noopener noreferrer"&gt;Initial Stage and PixiJS adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Abraham721/photoshot/blob/19f45ed6dd99800f910c0587baa53e73d46ab4fb/src/document.ts" rel="noopener noreferrer"&gt;Later document engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/react/react" rel="noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://github.com/pixijs/pixijs" rel="noopener noreferrer"&gt;PixiJS&lt;/a&gt;, and &lt;a href="https://www.photopea.com/learn/masks" rel="noopener noreferrer"&gt;Photopea's mask behavior guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full version, including architecture diagrams, rendered failure/recovery panels, the alpha profile and reproducibility files, read the &lt;a href="https://slohero.com/photoshot/guides/basic-tools/en/" rel="noopener noreferrer"&gt;complete research note on my website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Writing and source review were assisted by AI. This account is grounded in the cited project revisions and recorded diagnostic results.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
