<?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[FrameSprite Engineering Guides]]></title><description><![CDATA[FrameSprite Engineering Guides]]></description><link>https://framesprite.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Nov 2026 10:02:13 GMT</lastBuildDate><atom:link href="https://framesprite.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Release Atlas PNG and JSON as One Versioned Pair]]></title><description><![CDATA[Prevent stale atlas metadata from selecting new pixels by publishing an immutable image-and-data revision, validating it before activation and retaining…]]></description><link>https://framesprite.hashnode.dev/atlas-png-json-release-pair</link><guid isPermaLink="true">https://framesprite.hashnode.dev/atlas-png-json-release-pair</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:28:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/2b99096a-0993-4d8b-837c-2d9ee752ef9c.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When an atlas looks correct after a hard refresh but wrong for returning users, inspect the PNG and metadata as a pair. A new JSON rectangle can point into an old cached image, or old metadata can crop a new layout. Both requests may return successful responses. Publish a revision whose metadata names its matching image, validate that pair, and activate the revision only after both files are available.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>HTTP success is not proof that an image and its coordinates belong to the same build.</li>
<li>Keep revision URLs immutable and change a small release pointer after validation.</li>
<li>Retain the previous complete pair until clients no longer need it and rollback is safe.</li>
</ul>
</blockquote>
<h2>Reproduce the mismatch intentionally</h2>
<p>This fault-injection worksheet complements the existing <a href="https://www.framesprite.com/guides/sprite-asset-version-manifest-cache-handoff">sprite manifest revision guide</a>. Use its manifest contract, then deliberately interrupt and mix revisions as described below. The output is a release acceptance record, not a second manifest format.</p>
<p>In a disposable asset set, make revision A with a red square in the left cell and a blue square in the right cell. Make revision B swap the cells and update its frame rectangles accordingly. The character key <code>red</code> must select red under either matching revision.</p>
<p>Now pair A's metadata with B's image. The crop can remain within image bounds yet select blue. A structural validator may accept every rectangle and a network check may report two 200 responses. The fixture demonstrates why neither check proves semantic agreement.</p>
<p><a href="https://www.codeandweb.com/texturepacker/documentation/texture-settings">TexturePacker's data-file documentation</a> describes metadata containing sprite sizes and texture coordinates. Those coordinates are meaningful relative to a particular texture layout. Treat the exported image and data as one release unit even when they travel as separate requests.</p>
<h2>Give each release immutable names</h2>
<p>For example, place the pair under an application-owned revision directory:</p>
<pre><code class="language-text">assets/hero/r17/hero.png
assets/hero/r17/hero.json
assets/hero/r18/hero.png
assets/hero/r18/hero.json
</code></pre>
<p>Configure <code>r18/hero.json</code> to reference its own matching image according to the loader's schema. Do not add an invented manifest field and expect an unmodified engine loader to understand it. A small application release record can name the metadata entry point separately.</p>
<p>If a loader resolves image references relative to the JSON URL, keep that relationship intact. If it uses an asset base URL, verify the resolved request in the real application. Moving files into directories without checking resolution can turn a cache repair into a missing-image failure.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching">MDN's caching guide</a> explains versioned URLs and immutable-resource caching. Long cache lifetimes suit content that will not change at that URL. The mutable release pointer needs a different freshness policy. <code>no-cache</code> allows storage with revalidation; it is not another spelling of <code>no-store</code>.</p>
<p><a href="https://www.rfc-editor.org/rfc/rfc8246">RFC 8246</a> defines <code>immutable</code> for a response's freshness lifetime. It does not make related files a transaction or prevent an application from choosing a mismatched pair.</p>
<h2>Validate before changing the pointer</h2>
<p>Use a staging state that clients cannot select yet. Upload the complete new pair, then request both through the same public delivery path clients use. Confirm successful responses, image decoding, the expected dimensions and frame records, and the selected image URL.</p>
<p>Record a SHA-256 digest for each exact file in the release record. A digest comparison can detect a mismatched or altered body when the expected record is trusted. It does not, by itself, authenticate the publisher: an attacker able to replace both the bytes and the expected hashes defeats that comparison.</p>
<table>
<thead>
<tr>
<th>Validation</th>
<th>What it establishes</th>
<th>What it leaves open</th>
</tr>
</thead>
<tbody><tr>
<td>Both responses succeed</td>
<td>Files are retrievable</td>
<td>Whether they match each other</td>
</tr>
<tr>
<td>Rectangles fit the image</td>
<td>Coordinates are in bounds</td>
<td>Whether <code>red</code> selects red</td>
</tr>
<tr>
<td>Recorded hashes match</td>
<td>Bodies match expected bytes</td>
<td>Trustworthiness of the expected record</td>
</tr>
<tr>
<td>Named-color fixture passes</td>
<td>Loader selects the intended cells</td>
<td>Every production frame's artistic correctness</td>
</tr>
</tbody></table>
<p>The <a href="https://www.framesprite.com/guides/sprite-build-cache-export-settings-key">export-settings cache-key guide</a> covers the upstream risk of reusing output made under different settings. A correct delivery scheme cannot repair an already incorrect build artifact.</p>
<h2>Activate once, then preserve existing sessions</h2>
<p>Update the application's release pointer only after validation. Read and retain one revision choice for a loading operation rather than fetching a mutable “latest” reference again halfway through it. Otherwise, a rollout can split one operation across two releases.</p>
<p>Keep A available while B is active. A client that already obtained A's metadata may still request A's image later. Removing the old image immediately breaks a previously valid reference. Decide retention from the product's session and cache behavior, then test rollback to the complete A pair.</p>
<p>At a local filesystem boundary, <a href="https://www.framesprite.com/guides/sprite-atomic-replace-same-filesystem-stage">same-filesystem atomic replacement</a> can protect a particular replacement operation. It does not make two independent CDN requests transactional. Likewise, a <a href="https://www.framesprite.com/guides/sprite-release-tags-mutable-name">mutable release tag</a> needs an explicit resolution contract.</p>
<h2>Require the mixed-pair test to fail safely</h2>
<p>Start each row from a known complete A release and use a disposable B upload. Restore the control between faults. Record the release chosen before loading, the actual image and data hashes, the visible <code>red</code> result and the decision.</p>
<table>
<thead>
<tr>
<th>Injected condition</th>
<th>Expected acceptance decision</th>
</tr>
</thead>
<tbody><tr>
<td>A PNG with A JSON</td>
<td>Accept A; <code>red</code> selects red</td>
</tr>
<tr>
<td>B PNG with B JSON</td>
<td>Accept B only after both validations; <code>red</code> selects red</td>
</tr>
<tr>
<td>A PNG with B JSON</td>
<td>Reject the B candidate; keep active A</td>
</tr>
<tr>
<td>B PNG with A JSON</td>
<td>Reject the A pair as retrieved; use a separately validated complete fallback</td>
</tr>
<tr>
<td>Truncated or altered B PNG</td>
<td>Reject on digest or decode failure; do not activate B</td>
</tr>
<tr>
<td>B JSON or image returns 404</td>
<td>Treat B as incomplete; keep A selected</td>
</tr>
<tr>
<td>Offline with the complete pinned A pair cached</td>
<td>Accept A only under the application's stated offline policy</td>
</tr>
<tr>
<td>Offline with only A JSON cached</td>
<td>Report the missing dependency or use a complete verified fallback; never combine it with an arbitrary image</td>
</tr>
</tbody></table>
<p>The matching A and B fixtures must both select red for <code>red</code>. Mixed pairs must be rejected by the application's revision checks before acceptance; if a loader lacks those checks, the named-color mismatch exposes that missing safeguard. A successful decode is insufficient. Interrupt the upload before the second file and confirm the release pointer stays on A.</p>
<p>Finally, restore A through the normal pointer update and confirm a fresh load works. Save the selected revision, resolved URLs and hashes. That record is more useful than a screenshot saying the atlas “looked right” on one developer's warm cache.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering owns this AI-assisted article and has a commercial interest in FrameSprite. See the <a href="https://www.framesprite.com/about#workflow-team">team and editorial policy</a> and <a href="https://www.framesprite.com/contact">contact page</a>. Sources were consulted on 2026-10-30. The two-revision fixture is a proposed acceptance test; no CDN deployment or engine import was executed for this article.</p>
]]></content:encoded></item><item><title><![CDATA[Keep Animation Holds When Moving a Sprite Between Tools]]></title><description><![CDATA[Preserve a sprite animation’s frame order, unequal holds and cycle length with explicit duration units and a 50, 50, 200 millisecond handoff fixture.]]></description><link>https://framesprite.hashnode.dev/variable-duration-animation-handoff</link><guid isPermaLink="true">https://framesprite.hashnode.dev/variable-duration-animation-handoff</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:28:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/e8ecfd06-a35f-4d66-83bc-3e2e845abba8.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An animation can contain all the right images and still feel wrong because its importer replaced unequal frame holds with one frame rate. Preserve the ordered frame-duration pairs, their units and the playback multiplier. Then compare the cycle length and the hold pattern separately. Matching only the average speed can conceal a lost anticipation or impact pose.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>A sheet of pixels does not, by itself, describe how long each image stays on screen.</li>
<li>Absolute milliseconds, relative frame weights and added delays are different contracts.</li>
<li>Verify an unequal-duration fixture before importing the entire animation library.</li>
</ul>
</blockquote>
<h2>Start with three visibly different frames</h2>
<p>Create three labeled poses: A, B and C. Give them intended durations of 50, 50 and 200 milliseconds. Use forward looping playback with no transition blend and a speed multiplier of one. The complete cycle should last 300 milliseconds, while C occupies two-thirds of that cycle.</p>
<p>This is a synthetic fixture, not a recommendation for a character's cadence. Unequal values make a conversion fault obvious. Three frames at 100 milliseconds each would preserve the total cycle while destroying the intended rhythm.</p>
<p>Keep source frame identifiers alongside durations. Filename sorting is not an animation specification: <code>pose10</code> can land before <code>pose2</code> under a lexical sort. A compact atlas may also reuse one raster for repeated poses without implying that a time interval disappeared.</p>
<h2>Write the handoff record before the adapter</h2>
<p>Use an explicit intermediate record, such as the following conceptual JSON. It is an application-owned format for this example, not an Aseprite, Godot or PixiJS export schema.</p>
<pre><code class="language-json">{
  "loop": "forward",
  "durationUnit": "ms",
  "frames": [
    {"id": "A", "duration": 50},
    {"id": "B", "duration": 50},
    {"id": "C", "duration": 200}
  ]
}
</code></pre>
<p>Record direction and endpoint behavior as well. Reversing or ping-pong playback can alter which endpoints repeat. <a href="https://www.aseprite.org/docs/cli/">Aseprite's CLI documentation</a> describes export selection and naming controls; inspect the emitted data from the installed version rather than assuming the PNG carries its timeline. The <a href="https://www.framesprite.com/guides/aseprite-tag-playback-direction-turnaround">tag turnaround guide</a> addresses a separate endpoint-order check.</p>
<p>Reject negative, zero or non-finite values unless the destination has an explicitly supported meaning for them. JSON serialization should not silently turn an invalid duration into a usable-looking value. See the <a href="https://www.framesprite.com/guides/sprite-json-nonfinite-duration-export">non-finite duration check</a> for that boundary.</p>
<h2>Convert units deliberately</h2>
<p><a href="https://docs.godotengine.org/en/stable/classes/class_spriteframes.html">Godot's SpriteFrames reference</a> defines per-frame duration as a relative value. At 20 animation frames per second and playback speed one, weights <code>1, 1, 4</code> produce the intended <code>50, 50, 200</code> milliseconds. Passing <code>50, 50, 200</code> as relative weights would multiply the entire schedule.</p>
<p>For <a href="https://pixijs.download/v8.14.0/docs/scene.AnimatedSprite.html">PixiJS 8.14 AnimatedSprite</a>, use the frame-object form when preserving unequal holds. The <a href="https://github.com/pixijs/pixijs-skills/blob/main/skills/pixijs-scene-sprite/references/animated-sprite.md">PixiJS-maintained AnimatedSprite reference</a> specifies <code>{ texture, time }</code> with <code>time</code> in milliseconds. This fixture therefore passes <code>50, 50, 200</code> as frame times and keeps <code>animationSpeed</code> at one. A plain array of textures does not contain those duration values. Confirm the installed API before translating the example to another release.</p>
<table>
<thead>
<tr>
<th>Representation</th>
<th>Values for A, B, C</th>
<th>Required context</th>
</tr>
</thead>
<tbody><tr>
<td>Absolute milliseconds</td>
<td>50, 50, 200</td>
<td>Unit is ms; multiplier is one</td>
</tr>
<tr>
<td>Relative weights</td>
<td>1, 1, 4</td>
<td>Base rate is 20 fps; speed is one</td>
</tr>
<tr>
<td>Equal-rate approximation</td>
<td>100, 100, 100</td>
<td>Same total, wrong hold pattern</td>
</tr>
</tbody></table>
<p>An adapter should document the destination's meaning before assigning a property named <code>duration</code>. Similar names do not guarantee equivalent units. If a destination combines a base interval with an extra delay, sending an absolute duration as that delay adds time twice.</p>
<h2>Check the math without launching an engine</h2>
<p>This illustrative Python 3 example verifies only the conversion arithmetic:</p>
<pre><code class="language-python">durations_ms = [50, 50, 200]
fps = 20
weights = [ms * fps / 1000 for ms in durations_ms]
recovered_ms = [weight / fps * 1000 for weight in weights]
assert weights == [1.0, 1.0, 4.0]
assert recovered_ms == [50.0, 50.0, 200.0]
assert sum(recovered_ms) == 300.0
print(weights, sum(recovered_ms))
</code></pre>
<p>Now validate the real adapter. Log the imported frame IDs and configured durations before playback. Require A, B and C in order, the four-to-one C/A hold ratio, and a 300-millisecond nominal cycle at multiplier one. Reject the import if any value was silently dropped.</p>
<h2>Observe playback without overclaiming precision</h2>
<p>A displayed frame can remain visible until the next presentation opportunity. Therefore, engine timing logs and a screen recording do not have identical resolution. Record the engine version, update loop and capture rate when judging visible holds.</p>
<p>Use a long enough repeated sample to spot a sustained rate mismatch, but investigate skipped updates separately. The <a href="https://www.framesprite.com/guides/bevy-animation-timer-missed-ticks">Bevy missed-tick guide</a> explains one update-loop hazard. A correct metadata conversion does not establish how another engine catches up after a stall.</p>
<h2>Evidence and editorial responsibility</h2>
<p>This AI-assisted article is maintained by FrameSprite Engineering, which has a commercial interest in the product. See the <a href="https://www.framesprite.com/about#workflow-team">team and editorial policy</a> and <a href="https://www.framesprite.com/contact">contact page</a>. Sources were consulted on 2026-10-30. The fixture and arithmetic are illustrative, not recorded engine results.</p>
]]></content:encoded></item><item><title><![CDATA[Sprite Halos or Atlas Bleed? Run These Controls]]></title><description><![CDATA[Separate alpha interpretation, contaminated edge colors and neighboring atlas samples with isolated-image and background controls before repacking sprites.]]></description><link>https://framesprite.hashnode.dev/alpha-halo-atlas-bleed-diagnosis</link><guid isPermaLink="true">https://framesprite.hashnode.dev/alpha-halo-atlas-bleed-diagnosis</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:27:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/cd662d30-ca21-4dfa-bbb8-53553d03b13f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A dark sprite outline and a line sampled from a neighboring atlas cell need different fixes. First draw the sprite as a standalone image using the same alpha interpretation, filter and scale as the atlas version. Then compare both over light and dark backgrounds. A defect that survives isolation points away from neighboring atlas content, although the isolated control must preserve the relevant rendering settings.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Compare isolated and packed images before increasing padding.</li>
<li>Treat edge color, alpha representation and sampling as separate variables.</li>
<li>Verify the final mip and scale range; a one-scale screenshot does not validate every sampler footprint.</li>
</ul>
</blockquote>
<h2>Save the failing pair and its settings</h2>
<p>Keep the original source, packed texture and metadata. Record filter mode, mipmap use, texture compression, alpha mode and the scale at which the defect occurs. A new packing run that changes all of these cannot show which change helped.</p>
<p>Make a diagnostic copy with a saturated green neighbor beside the affected sprite. This artificial color is a tracer. If the artifact becomes green in the atlas version but is absent from the isolated control, neighboring texels are a stronger candidate. The test is intentionally conspicuous and should never replace the production artwork.</p>
<p><a href="https://www.codeandweb.com/texturepacker/documentation/texture-settings">TexturePacker's texture settings</a> distinguish spacing between shapes from extrusion of edge pixels. Empty space and repeated edge color are different inputs to the sampler. Their existence is useful evidence, but no fixed padding value is sufficient for every scale, mip level and renderer.</p>
<h2>Compare four controls</h2>
<p>Use identical output resolution and capture at native size. Avoid evaluating a screenshot after a chat app has resized or recompressed it.</p>
<table>
<thead>
<tr>
<th>Control</th>
<th>What stays fixed</th>
<th>What the result can suggest</th>
</tr>
</thead>
<tbody><tr>
<td>Standalone sprite, light background</td>
<td>Source pixels and draw settings</td>
<td>Dark fringe may be in colors or alpha handling</td>
</tr>
<tr>
<td>Standalone sprite, dark background</td>
<td>Same image and scale</td>
<td>Light fringe may reveal matte contamination</td>
</tr>
<tr>
<td>Atlas with saturated neighbor</td>
<td>Sprite art and alpha settings</td>
<td>Matching tracer color suggests neighbor sampling</td>
</tr>
<tr>
<td>Atlas at failing scale or mip</td>
<td>Intended presentation path</td>
<td>A clean 1:1 view alone was insufficient</td>
</tr>
</tbody></table>
<p>These are clues, not an automated diagnosis. Compression can alter edge pixels. A shader can add an outline. Multiple faults can coexist. Disable a custom effect only in a separate controlled run so the original failure remains reproducible.</p>
<h2>Check whether alpha was multiplied twice</h2>
<p>Straight-alpha colors store RGB independently of opacity. Premultiplied colors already include the alpha factor. The upload path and blending path must agree about that representation. <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices">MDN's WebGL guidance</a> discusses alpha handling as a pipeline choice, rather than a generic “make transparent” switch.</p>
<p>Here is an illustrative single-channel calculation in normalized values. A white straight-alpha sample has color <code>1.0</code> and alpha <code>0.5</code>. Over black, source-over gives a color contribution of <code>0.5</code>. A premultiplied sample already stores <code>0.5</code>; multiplying that value by alpha again gives <code>0.25</code>, a darker contribution. This arithmetic illustrates one mismatch. It does not model color-space conversion, quantization or a complete renderer.</p>
<p>Inspect the actual loader, not just an editor checkbox. Moving from an image element to an ImageBitmap can move where relevant upload options belong. The <a href="https://www.framesprite.com/guides/webgl-imagebitmap-upload-flip-premultiply-options">Reference: ImageBitmap upload guide</a> addresses that specific boundary.</p>
<h2>Separate matte repair from atlas repair</h2>
<p>If the isolated source already has pale or dark fringes, inspect partly transparent edge pixels. Their RGB values can contain a background color from an earlier compositing step. Repacking those same pixels cannot restore the intended edge colors.</p>
<p>Godot's <a href="https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html">Reference: image import documentation</a> describes alpha-border correction and premultiplied-alpha import options separately. Use the option that matches the diagnosed condition, then recheck the material and blend path. Applying every available alpha option is not a reliable sequence.</p>
<p>If the tracer-neighbor test implicates atlas sampling, inspect frame bounds, extrusion, padding and mip generation together. A wrongly offset UV rectangle can still sample a neighbor despite generous empty margins. A metadata mismatch can even select the wrong rectangle entirely.</p>
<p>Do not make a hard alpha threshold your first general repair. It changes the silhouette of soft effects. See <a href="https://www.framesprite.com/guides/gimp-threshold-alpha-lock-binary-sprite-edge">GIMP's threshold-alpha scope</a> before removing partial opacity, and <a href="https://www.framesprite.com/guides/pillow-paste-mask-source-over-alpha">Pillow's paste-mask behavior</a> when the edge changed during atlas assembly.</p>
<h2>Calibrate the diagnosis with two known faults</h2>
<p>Use two deliberately different fixtures before classifying production art. These are proposed renderer controls with computed expected values, not captured graphics results. Keep color values normalized and use a linear, straight-alpha source-over model for the arithmetic; disable compression and custom effects for these controls.</p>
<p>For case A, the intended edge is red RGB <code>(1,0,0)</code> at alpha <code>0.5</code>. Contaminate only its RGB to <code>(1,0.5,0.5)</code>, keeping alpha unchanged. Over black, the intended result is <code>(0.5,0,0)</code> but the contaminated result is <code>(0.5,0.25,0.25)</code>. Over white, they become <code>(1,0.5,0.5)</code> and <code>(1,0.75,0.75)</code>. Pack the same source with sampling confined to that texel. The fringe remains in both versions; changing the atlas neighbor cannot remove the source contamination.</p>
<p>For case B, use an opaque red subject texel and an opaque green neighbor. A deliberately chosen 50:50 linear sample across their boundary gives <code>(0.5,0.5,0)</code>, while the isolated red control remains <code>(1,0,0)</code>. This specifies a sampler input, not a claim that every UV or mip level produces equal weights. Adjust the test UV until the constructed control actually exercises the boundary, then record it.</p>
<pre><code class="language-python">def over(rgb, alpha, background):
    return tuple(c * alpha + b * (1 - alpha)
                 for c, b in zip(rgb, background))
assert over((1, 0, 0), 0.5, (0, 0, 0)) == (0.5, 0.0, 0.0)
assert over((1, 0.5, 0.5), 0.5, (0, 0, 0)) == (0.5, 0.25, 0.25)
assert over((1, 0, 0), 0.5, (1, 1, 1)) == (1.0, 0.5, 0.5)
assert over((1, 0.5, 0.5), 0.5, (1, 1, 1)) == (1.0, 0.75, 0.75)
assert tuple((r + g) / 2 for r, g in zip((1, 0, 0), (0, 1, 0))) == (0.5, 0.5, 0.0)
</code></pre>
<table>
<thead>
<tr>
<th>Known input or observed branch</th>
<th>Isolated control</th>
<th>Packed control</th>
<th>Decision to record</th>
</tr>
</thead>
<tbody><tr>
<td>A: contaminated source RGB</td>
<td>Pink contamination on both backgrounds</td>
<td>Same contamination when sampling stays within the sprite</td>
<td>Reject a neighbor-only explanation; inspect source colors and alpha handling separately</td>
</tr>
<tr>
<td>B: clean source, green boundary sample</td>
<td>Clean red</td>
<td>Green component appears at the deliberately sampled boundary</td>
<td>Retain UV/filter/neighbor hypothesis; reject source contamination as the only explanation</td>
</tr>
<tr>
<td>A plus B in one asset</td>
<td>Contamination survives isolation</td>
<td>Additional tracer contribution appears near the boundary</td>
<td>Retain both faults; repair and rerun each independently</td>
</tr>
<tr>
<td>Neither fault under the control settings</td>
<td>Matches its specified reference</td>
<td>Matches its specified reference</td>
<td>Mark only these controls clean; do not clear untested production scales</td>
</tr>
<tr>
<td>Control changed alpha mode, mip, shader or scale</td>
<td>Not comparable</td>
<td>Not comparable</td>
<td>Inconclusive: restore matched settings before eliminating either hypothesis</td>
</tr>
</tbody></table>
<p>Save the input RGBA values, background, UV, filter, mip condition and observed color beside each decision. A real capture may differ from these linear values because of transfer functions or quantization; compare in a defined color space and tolerance. Failure to reproduce the known bad case means the diagnostic control itself is not ready to clear production art.</p>
<h2>Define what a passing edge looks like</h2>
<p>Keep a reference of the intended source edge over both backgrounds. Require the atlas version to agree with that reference within the project's chosen compression tolerance at every supported scale and mip condition in the test plan. The green tracer must not appear in the subject edge.</p>
<p>Archive the corrected settings and repeat after the real neighbor art is restored. This checks the actual delivery asset. A passing tracer control is evidence about the tested sampling path, not proof that every device or custom shader will render it identically.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering prepared this AI-assisted article and has a commercial interest in FrameSprite. See the <a href="https://www.framesprite.com/about#workflow-team">team and editorial policy</a> and <a href="https://www.framesprite.com/contact">contact page</a>. Sources were consulted on 2026-10-30. Controls and numeric values are illustrative; no exporter, browser graphics context or engine was executed for this article.</p>
]]></content:encoded></item><item><title><![CDATA[Find Pixel Jitter Before Changing Your Sprite Art]]></title><description><![CDATA[Use a controlled camera, scale and frame matrix to separate source-frame drift, fractional pixel scaling and camera motion in a pixel-art game.]]></description><link>https://framesprite.hashnode.dev/pixel-jitter-camera-scale-test</link><guid isPermaLink="true">https://framesprite.hashnode.dev/pixel-jitter-camera-scale-test</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:27:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/064809b4-1f0d-4a07-86e3-fb2b26309ddf.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To diagnose pixel-art jitter, freeze one source frame and change only the camera, then restore a fixed camera and change only the animation. That split tells you whether the defect follows scene sampling or the artwork. Switching filters, snapping positions and repainting frames at once removes the evidence you need to find the cause.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Sharp edges can still appear to shake when the sampling phase changes.</li>
<li>A non-integer display scale cannot give every source pixel the same integer width.</li>
<li>Keep a stationary crosshair in the test scene so camera and character motion remain distinguishable.</li>
</ul>
</blockquote>
<h2>Build a tiny scene that can fail visibly</h2>
<p>Use a copy of the project with a checkerboard, a single-pixel vertical stripe and one character. Start with a fixed camera, no rotation, a fixed window size and an integer scale. Record the source image, viewport dimensions, actual output dimensions and filter setting. In a browser build, also record browser zoom and the canvas backing dimensions.</p>
<p>Do not compare an editor preview at one scale with a game window at another. <a href="https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html">Godot's multiple-resolution documentation</a> separates the base design size from the eventual window and explains integer scaling. Those are distinct stages in the image's route to the display.</p>
<p>A real <a href="https://forum.godotengine.org/t/camera-jittering-on-smoothing/65392">Godot camera-smoothing question</a> illustrates the symptom users describe. A forum report establishes that someone encountered it; it does not prove which setting is responsible in your project or how common the problem is.</p>
<h2>Run one change per row</h2>
<p>Keep the same art and save a short capture for each condition. The observations below are diagnostic branches, not guaranteed outcomes on every renderer.</p>
<table>
<thead>
<tr>
<th>Test</th>
<th>Change from control</th>
<th>If the symptom appears here</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>Nothing: one frame, fixed camera, integer scale</td>
<td>Inspect source pixels and the complete output path</td>
</tr>
<tr>
<td>B</td>
<td>Animate frames; camera remains fixed</td>
<td>Compare contacts, offsets and per-frame transforms</td>
</tr>
<tr>
<td>C</td>
<td>Freeze the frame; move the camera</td>
<td>Inspect camera timing and display sampling</td>
</tr>
<tr>
<td>D</td>
<td>Freeze scene; use a fractional output scale</td>
<td>Inspect uneven source-pixel coverage</td>
</tr>
<tr>
<td>E</td>
<td>Restore integer scale; enable camera smoothing</td>
<td>Compare the camera and sprite update paths</td>
</tr>
</tbody></table>
<p>Pause between tests and check that the reverted setting really returned to its control value. A camera with smoothing enabled can retain a transitional position after you stop moving its target.</p>
<p>If B fails while C is clean, do not begin by changing the camera. If C fails with a frozen frame, repainting the animation cannot fix the cause demonstrated by that test. More than one row can fail; then you have more than one condition to investigate.</p>
<h2>Understand the fractional-scale fixture</h2>
<p>Consider eight source columns enlarged to twenty output columns. The scale is <code>20 / 8 = 2.5</code>. Eight columns with equal integer widths cannot total twenty: using width two gives sixteen and using width three gives twenty-four. Some source columns must occupy a different number of output columns, or the renderer must mix values.</p>
<p>With nearest sampling, the edges can remain hard while column widths vary. As the mapping shifts, the distribution can change. This arithmetic explains why “it is sharp, so the scale must be correct” is not a sufficient check. It does not predict the exact column assignment of every graphics API.</p>
<p>For a control, enlarge the same stripe pattern from eight to twenty-four columns. At scale three, equal three-column coverage is possible. Compare this control with the fractional case at native screenshot resolution, without another viewer resizing the capture.</p>
<h2>Choose a correction at the stage that failed</h2>
<p>If the source-frame test fails, inspect the character's registration point before display settings. If the fractional-scale test fails, choose a deliberate fit strategy: integer scaling with unused borders, a cropped view, or an accepted fractional presentation. Each choice trades available screen area against pixel uniformity.</p>
<p>For Unity 6 URP projects, the <a href="https://docs.unity.com/en-us/engine/6000.0/manual/unity2d/2d-urp/2d-pixelperfect/prep-sprites">official Pixel Perfect Camera setup</a> aligns sprite pixels-per-unit with the camera and documents sprite preparation. That recipe is pipeline-specific. It should not be applied as a universal camera package instruction to Godot or a browser canvas.</p>
<p>Nearest filtering alone does not make a camera's world position integral in the final output grid. Conversely, rounding a physics object's world position can change motion. Test a presentation-layer correction separately and check the effect at the actual output resolution.</p>
<h2>Keep a useful acceptance record</h2>
<p>Require the stationary stripe control to remain stable, the chosen planted contact to stay fixed during the selected idle frames, and camera movement to meet your project's presentation requirement. Write down any permitted shimmer at fractional scales rather than claiming perfect output at every window size.</p>
<p>Further checks distinguish <a href="https://www.framesprite.com/guides/sprite-responsive-currentsrc-atlas-coordinate-contract">responsive-image source coordinates</a>, <a href="https://www.framesprite.com/guides/canvas-putimagedata-bypasses-scale">Canvas pixel writes that bypass transforms</a> and <a href="https://www.framesprite.com/guides/haxeflixel-camera-follow-custom-deadzone">a HaxeFlixel camera deadzone</a>. A capture can diagnose one path without validating those other paths.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering owns this AI-assisted article and has a commercial interest in the product. See the <a href="https://www.framesprite.com/about#workflow-team">team and editorial policy</a> and <a href="https://www.framesprite.com/contact">contact page</a>. Documentation and the question thread were consulted on 2026-10-30. The scale example is a geometric illustration, not a measured engine benchmark. No engine scene was run.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Sprite Foot Drift After Atlas Trimming]]></title><description><![CDATA[Trace foot drift through source coordinates, trim offsets and pivots, using a two-frame fixture that separates broken metadata from intentional motion.]]></description><link>https://framesprite.hashnode.dev/sprite-jump-trim-pivot-contract</link><guid isPermaLink="true">https://framesprite.hashnode.dev/sprite-jump-trim-pivot-contract</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:26:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/6cc36772-92f2-48ee-98b3-4e88c61314a8.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If a character's planted foot jumps when an animation changes frames, compare that foot in the original canvas coordinates before moving the game object. Independent trimming changes a frame's stored rectangle. It should not silently change where the character stands. The fault can sit in missing trim offsets, an anchor based on the cropped rectangle, or the artwork itself.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Keep an original coordinate space for contact points and pivots.</li>
<li>Use two frames with deliberately different transparent margins to expose a broken importer.</li>
<li>Validate the visible contact marker as well as metadata; a correct rectangle can contain misplaced art.</li>
</ul>
</blockquote>
<h2>Freeze the scene before adjusting the feet</h2>
<p>Use a disposable two-frame animation, a stationary camera, an unrotated character and unit scale. Put a bright marker on the planted heel in both frames. Disable code that moves the character while this test runs. Otherwise, camera motion and locomotion can hide an import error.</p>
<p>Record four things for each frame: original canvas size, trimmed rectangle within that canvas, chosen pivot and the marker's position. These are different values. A rectangle's position on the atlas tells the loader where to read pixels, while its trim offset tells the renderer how those pixels fit into the original canvas.</p>
<p><a href="https://www.codeandweb.com/texturepacker/knowledgebase/keep-animation-alignment-with-trim">CodeAndWeb's alignment guidance</a> recommends exporting framework metadata with trimmed sprites. The relevant reader check is whether the consumer actually uses that metadata. A PNG-only export cannot communicate the transparent area removed from each frame.</p>
<h2>Give the coordinate contract numbers</h2>
<p>Use this two-frame coordinate ledger as a worksheet beside the existing <a href="https://www.framesprite.com/guides/sprite-pivot-anchor-origin">pivot and origin guide</a> and <a href="https://www.framesprite.com/guides/trimmed-sprite-atlas-metadata">trimmed-atlas metadata explanation</a>. The extra deliverable here is a comparison record with an expected result for each frame, rather than another exporter setup recipe.</p>
<p>This synthetic fixture uses top-left origins, X to the right and Y downward. Coordinates identify geometric positions, not integer pixel indices: the bottom boundary of a 64-pixel-high canvas is Y=64. The original canvas is 64×64 and its pivot is <code>(32, 60)</code>.</p>
<table>
<thead>
<tr>
<th>Frame</th>
<th>Trim offset</th>
<th>Heel in trimmed coordinates</th>
<th>Heel in original coordinates</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>(12, 8)</td>
<td>(20, 52)</td>
<td>(32, 60)</td>
</tr>
<tr>
<td>B</td>
<td>(8, 4)</td>
<td>(24, 56)</td>
<td>(32, 60)</td>
</tr>
</tbody></table>
<p>For this unrotated fixture, reconstruct the original heel as <code>trim_offset + local_heel</code>. Its position relative to the pivot is then <code>(0, 0)</code> in both frames. If a consumer instead places the local heels directly, B differs from A by <code>(4, 4)</code>. That is a useful failure signature, not a reason to paint four pixels off the second frame.</p>
<p>The following Python 3 arithmetic example has no engine dependency. It illustrates the contract; it is not an engine import test.</p>
<pre><code class="language-python">pivot = (32, 60)
frames = [((12, 8), (20, 52)), ((8, 4), (24, 56))]
contacts = [tuple(t + p - a for t, p, a in zip(trim, foot, pivot))
            for trim, foot in frames]
assert contacts == [(0, 0), (0, 0)]
print(contacts)
</code></pre>
<p><a href="https://pixijs.com/8.x/guides/components/textures">PixiJS's texture model</a> distinguishes the atlas frame, original dimensions and trim region. Other engines may encode equivalent information differently. Do not paste field names from one schema into another importer without checking its documented format.</p>
<h2>Compare the importer with an untrimmed control</h2>
<p>Export the same source twice to unused paths: once without trimming and once with trimming plus the correct framework data. Keep frame order, pivot, scale and source art unchanged. Import both through the production loader, not a separate image viewer.</p>
<p>Place their pivots at the same fixed world coordinate and step each frame manually. Overlay the heel markers against a fixed crosshair. If only the trimmed version moves, inspect the metadata path. If both move, compare original heel coordinates and any per-frame transform first.</p>
<p>Check the draw API's anchor convention, too. A normalized anchor of <code>(0.5, 1)</code> is not a promise that every frame uses a common original canvas. A custom cropped-image renderer may interpret it against each crop. <a href="https://www.codeandweb.com/texturepacker/documentation/texture-settings">TexturePacker's trim modes</a> have distinct positioning contracts, so the mode and consumer must agree.</p>
<h2>Accept the correction without erasing animation</h2>
<p>For the synthetic fixture, require a zero heel-to-pivot offset in both frames and identical heel positions between trimmed and untrimmed imports. Repeat at a larger integer scale so a one-pixel error is easy to see. Save the two exports and the import settings with the result.</p>
<p>Apply the same method to a real idle frame and a real transition frame. Choose a genuinely planted contact point. A lifting toe, squash or intentional root movement should move; forcing every silhouette to share a bottom edge can damage the animation.</p>
<p>Engine-specific follow-ups cover <a href="https://www.framesprite.com/guides/bevy-anchor-component-ground-contact">Bevy ground-contact anchors</a>, <a href="https://www.framesprite.com/guides/haxeflixel-scale-updatehitbox-foot-anchor">HaxeFlixel scale and hitbox changes</a> and <a href="https://www.framesprite.com/guides/phaser-spritesheet-inside-atlas-untrimmed-grid-contract">Phaser's untrimmed-grid contract</a>. They address different consumers of the same handoff problem.</p>
<h2>Evidence and editorial responsibility</h2>
<p>This AI-assisted article is maintained by FrameSprite Engineering, which has a commercial interest in FrameSprite. See the <a href="https://www.framesprite.com/about#workflow-team">team and editorial policy</a> and <a href="https://www.framesprite.com/contact">contact page</a>. Primary documentation was consulted on 2026-10-30. The numeric fixture is illustrative; no game engine or exporter was executed for this article.</p>
]]></content:encoded></item><item><title><![CDATA[Keep Sprite Equipment Attached When Facing Left]]></title><description><![CDATA[Define socket coordinates, trim offsets and one owner for mirroring so a sprite character can turn without leaving its weapon behind.]]></description><link>https://framesprite.hashnode.dev/equipment-socket-flip-contract</link><guid isPermaLink="true">https://framesprite.hashnode.dev/equipment-socket-flip-contract</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:24:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/66cc472d-890d-4581-a05a-ded92c7d5e51.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Store an equipment socket in a declared character coordinate space and mirror it exactly once. Texture flipping, transform mirroring and atlas trimming affect different parts of that calculation. A weapon that stays on the wrong side after turning often reveals an unclear coordinate contract rather than bad animation artwork.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Record sockets against the original frame canvas, with an explicit origin and units.</li>
<li>Separate texture-only flipping from mirroring a visual parent and its children.</li>
<li>Test an off-center socket and multiple trim offsets before using production art.</li>
</ul>
</blockquote>
<p>The method below assumes a 2D character with frame-based artwork. It defines its own mathematical convention instead of claiming that every engine uses the same pivot or flip behavior. Adapt the conversion at the importer boundary.</p>
<h2>Write the coordinate contract before the code</h2>
<p>For this example, the original canvas is 64 × 64 units, with its top-left edge at <code>(0, 0)</code>, positive X to the right and positive Y downward. Coordinates are continuous edge coordinates: the right edge is X = 64. The character's local origin is an anchor at <code>(32, 56)</code> in that canvas. A hand socket sits at <code>(46, 28)</code>.</p>
<p>Therefore the unmirrored socket relative to the character origin is <code>(14, -28)</code>. These values are a synthetic fixture, not measurements from a shipped asset. With the character origin at world position <code>(100, 200)</code> and no rotation or scale, the socket is at <code>(114, 172)</code>.</p>
<p>Do not mix this convention with integer pixel indices. A mirrored pixel index uses <code>W - 1 - x</code>; a continuous point mirrored across the center of a canvas uses <code>W - x</code>. For a general anchor, reflecting a local offset around that anchor is simpler: negate its local X. Mirroring around the character anchor is not always the same as mirroring around the canvas center.</p>
<h2>Keep trimming out of the logical socket</h2>
<p>Two trimmed frames may place the same logical hand at different positions in their stored rectangles. Suppose frame A has trim origin <code>(8, 4)</code> and frame B has <code>(12, 2)</code>. The stored socket coordinates are <code>(38, 24)</code> and <code>(34, 26)</code>, respectively. Adding the corresponding trim origins restores <code>(46, 28)</code> in both cases.</p>
<table>
<thead>
<tr>
<th>Frame</th>
<th>Trim origin</th>
<th>Socket in stored rectangle</th>
<th>Socket in original canvas</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td><code>(8, 4)</code></td>
<td><code>(38, 24)</code></td>
<td><code>(46, 28)</code></td>
</tr>
<tr>
<td>B</td>
<td><code>(12, 2)</code></td>
<td><code>(34, 26)</code></td>
<td><code>(46, 28)</code></td>
</tr>
</tbody></table>
<p>Prefer storing the logical coordinate directly when the delivery format permits it. If the exporter stores a trimmed coordinate, include the translation needed to reconstruct the original one. PixiJS's <a href="https://pixijs.com/8.x/guides/components/textures">texture model</a> distinguishes original dimensions, trim metadata and texture frames; those fields should not be treated as interchangeable coordinates.</p>
<p>Rotated atlas entries require their documented rotation conversion before this translation. The table deliberately excludes packing rotation so the first fixture isolates trimming and mirroring.</p>
<h2>Choose one mirroring owner</h2>
<p>There are two valid designs, but combining them blindly mirrors the socket twice.</p>
<p>In a <strong>visual-parent design</strong>, place the character artwork and attachment socket under the same visual transform. A horizontal scale of -1 on that parent mirrors both local positions. Leave the socket's local X at 14; do not negate it again. Keep gameplay movement and collision ownership outside that visual subtree when they should not inherit the reflection.</p>
<p>In a <strong>texture-only design</strong>, flip the character image and explicitly compute the attachment's facing-dependent local position. In Godot, <a href="https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-flip-h"><code>Sprite2D.flip_h</code></a> is documented as flipping the texture. <a href="https://docs.godotengine.org/en/stable/classes/class_node2d.html"><code>Node2D</code></a> describes parent transforms that move, rotate and scale children. Do not assume a texture flip is equivalent to a negative parent scale.</p>
<p>For the defined anchor-space fixture, the left-facing socket offset is <code>(-14, -28)</code>, yielding world position <code>(86, 172)</code>. Decide separately how the weapon's image, its own grip anchor and its draw order respond to facing. Correct socket placement does not automatically orient a sword blade correctly.</p>
<h2>Run the acceptance test before adding animation</h2>
<p>Use a visible cross at the socket and a small asymmetric test weapon. First freeze frame A. Record right-facing <code>(114, 172)</code> and left-facing <code>(86, 172)</code>. Then switch to frame B without moving the character. The socket should remain at those same world positions because the original-canvas hand coordinate did not change.</p>
<p>This illustrative calculation checks the translation contract independently of an engine:</p>
<pre><code class="language-python">anchor, world = (32, 56), (100, 200)
for trim, stored in [((8, 4), (38, 24)), ((12, 2), (34, 26))]:
    original = tuple(a + b for a, b in zip(trim, stored))
    local = tuple(a - b for a, b in zip(original, anchor))
    assert original == (46, 28) and local == (14, -28)
    assert (world[0] + local[0], world[1] + local[1]) == (114, 172)
    assert (world[0] - local[0], world[1] + local[1]) == (86, 172)
</code></pre>
<p>After that passes, add rotation, parent scale and animated socket movement individually. A following weapon that lags by one update may instead have a <a href="https://www.framesprite.com/guides/bevy-globaltransform-propagation-socket-lag">transform-propagation timing problem</a>. A jump on detach needs a <a href="https://www.framesprite.com/guides/godot-reparent-dropped-equipment-global-transform">local-to-global reparenting check</a>. For grouped sprites, verify <a href="https://www.framesprite.com/guides/haxeflixel-spritegroup-member-coordinates">whether member positions are local or world coordinates</a>.</p>
<h2>Reject an incomplete socket timeline</h2>
<p>Correct coordinates on frame A do not repair a missing socket on frame B. Define the required <code>(action, frame ID, socket ID)</code> keys from the exported animation's actual frame IDs, then compare them with the socket records before playing it. This independent illustrative guard deliberately omits one required key:</p>
<pre><code class="language-python">expected = {('attack', frame, 'hand') for frame in ['A', 'B']}
actual = {('attack', 'A', 'hand')}
missing = expected - actual
assert missing == {('attack', 'B', 'hand')}
</code></pre>
<p>For this fixture, reject the handoff with the exact missing key. If production intentionally holds a socket from the previous pose, encode that policy explicitly instead of using accidental stale state. Validate duplicate keys before constructing a dictionary, because a dictionary can hide an overwritten record.</p>
<p>Approve the attachment only after both facings, both trim cases and the actual runtime hierarchy pass. The arithmetic validates this coordinate convention; it does not prove an importer, physics shape or every attachment animation implements it.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering prepared this AI-assisted guide using primary engine documentation and a synthetic coordinate fixture. The team has a commercial interest in FrameSprite. No engine execution or production attachment test is claimed. <a href="https://www.framesprite.com/about#workflow-team">Editorial ownership and contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Trace a Sprite Hitch From Download to First Draw]]></title><description><![CDATA[Use controlled cold and warm runs to distinguish sprite transfer, decode, parsing, GPU preparation and first-draw work before choosing a preload fix.]]></description><link>https://framesprite.hashnode.dev/first-sprite-appearance-hitch</link><guid isPermaLink="true">https://framesprite.hashnode.dev/first-sprite-appearance-hitch</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:23:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/2d08964d-c908-4daa-b61a-fa9c41083984.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When a sprite hitches on first appearance, trace its path from request to first visible draw. A completed download or resolved asset promise does not, by itself, identify the remaining work. Compare controlled cold and warm runs before selecting background loading, texture preparation or a different rendering path.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Define what each “ready” marker actually observes.</li>
<li>Separate cache state, scene construction and renderer preparation in the test matrix.</li>
<li>Change one stage at a time and keep the trace that supports the improvement.</li>
</ul>
</blockquote>
<p>This procedure is for a developer who can add timing markers and capture a browser performance trace. It is a diagnostic plan, not a benchmark result. Browser, renderer, graphics API and device differences can change which stage is expensive.</p>
<h2>Record boundaries that the application can observe</h2>
<p>Use names that describe events rather than assumptions. <code>assetPromiseResolved</code> is more precise than <code>gpuReady</code> unless the API actually guarantees GPU preparation. Include the asset revision, renderer version, viewport and device in the trace notes.</p>
<table>
<thead>
<tr>
<th>Marker</th>
<th>What it establishes</th>
<th>What it leaves open</th>
</tr>
</thead>
<tbody><tr>
<td>Request begins/response completes</td>
<td>Observed network activity</td>
<td>Decode, parsing and rendering cost</td>
</tr>
<tr>
<td>Asset promise resolves</td>
<td>The loader's documented completion point</td>
<td>Every renderer-specific preparation step</td>
</tr>
<tr>
<td>Scene object constructed</td>
<td>Application object exists</td>
<td>Its first draw is cheap</td>
</tr>
<tr>
<td>Preparation resolves</td>
<td>The selected preparation API finished</td>
<td>All future shader/material variants are ready</td>
</tr>
<tr>
<td>First visible draw</td>
<td>Content was presented</td>
<td>The hitch's underlying cause</td>
</tr>
</tbody></table>
<p>Add markers around object creation too. A complex scene can stall after assets are ready because the application constructs many objects in one task. Labeling that time as “texture loading” sends the investigation toward the wrong layer.</p>
<p>The <a href="https://pixijs.com/8.x/guides/components/textures">PixiJS texture guide</a> distinguishes loading from preparation for rendering and points to Prepare. Treat that as a reason to measure a boundary, not proof that every first-frame hitch is a texture upload.</p>
<h2>Use a matrix that changes one condition</h2>
<p>Make a disposable scene containing one representative atlas and a fixed number of sprites. Keep animation, filters and object count constant across runs. Record several repetitions; retain individual samples instead of reporting only the fastest one.</p>
<table>
<thead>
<tr>
<th>Run</th>
<th>Asset state</th>
<th>Scene construction</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>Newly loaded through the normal path</td>
<td>Immediately before display</td>
<td>Baseline user path</td>
</tr>
<tr>
<td>B</td>
<td>Same loaded resources remain live</td>
<td>Reconstruct the scene</td>
<td>Tests whether network loading explains recurrence</td>
</tr>
<tr>
<td>C</td>
<td>Same resources, explicit supported preparation</td>
<td>Same construction as B</td>
<td>Tests preparation's contribution</td>
</tr>
<tr>
<td>D</td>
<td>Same as C</td>
<td>Scene constructed earlier</td>
<td>Tests construction scheduling</td>
</tr>
</tbody></table>
<p>Define “cold” in the notes. A new scene, a new renderer and an emptied HTTP cache are different states. Do not say a run is cold merely because the sprites were hidden and shown again.</p>
<p>Keep a measurement log with fields for <code>run_id</code>, <code>asset_revision</code>, <code>http_cache_state</code>, <code>renderer_recreated</code>, <code>asset_promise_ms</code>, <code>construction_ms</code>, <code>prepare_ms</code>, <code>first_visible_frame_ms</code> and <code>trace_file</code>. Mark unavailable boundaries as unavailable. In particular, a JavaScript marker after a render call is not automatically a GPU-completion timestamp. Disable unrelated AI, gameplay spawning and network polling in the isolated scene, then restore them in a separate integration run.</p>
<p>If B hitches like A despite no new image request, investigate work after transfer. If C changes the first draw but not construction time, preparation is a useful lead. If D changes the visible stall while C does not, inspect application work. These are diagnostic interpretations; a trace may show overlapping causes.</p>
<h2>Make preload completion explicit</h2>
<p>Background loading can move work earlier, but the application still needs a documented readiness barrier before it depends on those assets. The <a href="https://pixijs.com/8.x/guides/components/assets/background-loader">PixiJS background-loader documentation</a> separates starting background work from loading a bundle that the current screen needs. See the focused <a href="https://www.framesprite.com/guides/pixijs-background-load-bundle-not-ready-barrier">background-bundle readiness guide</a> before treating a scheduling call as completion.</p>
<p>For PixiJS 8, use the Prepare interface supported by the exact installed release, where appropriate. Verify its import or extension registration and accepted upload targets in that release's API documentation. Avoid copying a Prepare call from another major version. The <a href="https://www.framesprite.com/guides/pixijs-loaded-texture-first-gpu-upload-hitch">loaded-texture first-upload guide</a> covers that narrower case; this matrix adds network, construction and repeated-display controls around it.</p>
<p>Do not insert a blocking graphics query to make a measurement “more exact.” <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices">MDN's WebGL best practices</a> warns about blocking API calls. A measurement method can create the stall it purports to diagnose. A browser trace also does not necessarily expose a clean per-texture GPU duration.</p>
<h2>Set an acceptance rule before changing the loader</h2>
<p>Choose the device and frame-time requirement from the game's own target. Record the baseline trace, apply one change, and repeat the same run matrix. Require correct artwork, complete animation playback and acceptable first-appearance behavior across repeated entries. Check memory retention after exit: retaining every texture forever can hide reload cost by creating another problem.</p>
<p>In a factory-based engine, inspect resource loading independently from instance creation; the <a href="https://www.framesprite.com/guides/defold-dynamic-factory-first-spawn-resource-preload">Defold first-spawn guide</a> illustrates that distinction without implying that its API maps directly onto PixiJS.</p>
<p>If the hitch remains after prepared textures and early construction, keep the trace and investigate material variants, shader work, layout, garbage collection or unrelated tasks visible at that moment. Report what the evidence isolates. “Preloaded” is an implementation choice, not an acceptance result.</p>
<h2>Evidence and editorial responsibility</h2>
<p>This AI-assisted diagnostic procedure was prepared by FrameSprite Engineering, which has a commercial interest in FrameSprite. It synthesizes primary runtime documentation and proposes controlled experiments; no device timing or speedup is claimed. <a href="https://www.framesprite.com/about#workflow-team">Editorial ownership and contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Budget Sprite Memory Beyond the PNG File Size]]></title><description><![CDATA[Estimate decoded and GPU sprite texture costs, count shared sources once, and include old-new atlas overlap in a practical memory worksheet.]]></description><link>https://framesprite.hashnode.dev/sprite-texture-memory-budget</link><guid isPermaLink="true">https://framesprite.hashnode.dev/sprite-texture-memory-budget</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:20:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/53757027-bcdc-4f82-98ea-43066ec94dc4.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Budget sprite memory from texture dimensions, pixel formats and simultaneously live resources. The PNG download size is a separate network cost. A highly compressed image can still require a large decoded surface, a GPU allocation and temporary copies during loading or scene transitions.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Start with an explicit pixel-format assumption.</li>
<li>Count a shared atlas source once, then count separate copies and render targets.</li>
<li>Budget the transition's peak working set, not only the final scene.</li>
</ul>
</blockquote>
<p>This worksheet is for a developer choosing atlas sizes or investigating memory growth. It provides a reproducible estimate. It cannot report a browser's total memory or establish a universal safe texture budget.</p>
<h2>Separate the resources before adding the numbers</h2>
<p>Create one row per unique backing resource, not per visible character. Include its dimensions, format, mip policy, ownership and expected release point. Keep encoded response bytes in a different column so a download optimization is not mistaken for a GPU-memory optimization.</p>
<p>PixiJS 8 distinguishes a <a href="https://pixijs.com/8.x/guides/components/textures">TextureSource from Texture views</a>. Several atlas frame views can reference one source. Conversely, independently decoded copies or render targets should not be collapsed into that one row merely because they originated from the same PNG.</p>
<table>
<thead>
<tr>
<th>Resource</th>
<th>Count in the worksheet</th>
<th>Do not assume</th>
</tr>
</thead>
<tbody><tr>
<td>Downloaded PNG</td>
<td>Encoded transfer/storage bytes</td>
<td>Its size equals decoded memory</td>
</tr>
<tr>
<td>Decoded RGBA surface</td>
<td>Width × height × 4 under the stated format</td>
<td>The browser retains exactly one copy</td>
</tr>
<tr>
<td>GPU texture</td>
<td>Dimensions, format and allocated mip levels</td>
<td>Browser heap tools show all GPU costs</td>
</tr>
<tr>
<td>Frame view</td>
<td>Lightweight metadata, recorded separately</td>
<td>Each view duplicates the atlas pixels</td>
</tr>
<tr>
<td>Render target or snapshot</td>
<td>Its own dimensions and format</td>
<td>It shares the original atlas allocation</td>
</tr>
</tbody></table>
<p>For browser applications, <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices">MDN's WebGL best practices</a> recommends deliberate memory budgeting and notes the absence of a portable total-VRAM query. Measurements on target devices remain necessary.</p>
<h2>Calculate a transparent baseline</h2>
<p>Assume an uncompressed RGBA8 image: four bytes per pixel, one base level, no row padding or implementation overhead. A 2048 × 2048 surface then has 16,777,216 bytes, or 16 MiB. A 4096 × 4096 surface has 64 MiB under the same assumptions. Doubling both dimensions multiplies area and baseline storage by four.</p>
<p>For a square power-of-two image, a full mip chain has progressively smaller levels down to 1 × 1. Summing their areas gives a little less than four-thirds of the base-level pixel count. That estimate concerns the pixel levels, not the driver's exact allocation.</p>
<p>This illustrative Python calculation makes the units and mip assumption explicit:</p>
<pre><code class="language-python">def rgba8_bytes(side, full_mips=False):
    assert isinstance(side, int) and side &gt; 0 and side &amp; (side - 1) == 0
    sizes = []
    while True:
        sizes.append(side * side * 4)
        if not full_mips or side == 1:
            return sum(sizes)
        side //= 2
assert rgba8_bytes(2048) == 16_777_216
assert rgba8_bytes(2048, True) == 22_369_620
</code></pre>
<p>Block-compressed textures, floating-point targets and browser-internal representations need different calculations. The formula also does not include meshes, audio, application objects or the canvas back buffer. State these exclusions beside the result rather than burying them in a footnote.</p>
<h2>Include the old-new overlap</h2>
<p>Now model a transition from an old 2048 atlas to a new 4096 atlas. Assume each live atlas has one CPU RGBA8 copy and one GPU RGBA8 base-level copy. Also assume the new loader temporarily holds one additional full-size RGBA staging copy. Mipmaps are off throughout this example.</p>
<table>
<thead>
<tr>
<th>Simultaneously live allocation</th>
<th align="right">Baseline, MiB</th>
<th align="right">Half-resolution fallback, MiB</th>
</tr>
</thead>
<tbody><tr>
<td>Old 2048 CPU surface</td>
<td align="right">16</td>
<td align="right">16</td>
</tr>
<tr>
<td>Old 2048 GPU base level</td>
<td align="right">16</td>
<td align="right">16</td>
</tr>
<tr>
<td>New decoded surface</td>
<td align="right">64</td>
<td align="right">16</td>
</tr>
<tr>
<td>New GPU base level</td>
<td align="right">64</td>
<td align="right">16</td>
</tr>
<tr>
<td>Temporary new-image RGBA staging copy</td>
<td align="right">64</td>
<td align="right">16</td>
</tr>
<tr>
<td>Modeled transition peak</td>
<td align="right">224</td>
<td align="right">80</td>
</tr>
<tr>
<td>New steady state after old and temporary release</td>
<td align="right">128</td>
<td align="right">32</td>
</tr>
</tbody></table>
<p>The fallback column reduces the new atlas to 2048 × 2048 while preserving the old resource during the transition. It assumes a lower-resolution alternative has been authored and approved; stretching a smaller texture does not restore lost detail. If your loader does not create the staging copy, remove that row with supporting evidence instead of silently accepting its cost.</p>
<p>These values are not measurements. Their purpose is to expose a lifecycle assumption that a final-scene-only spreadsheet misses. Add snapshots and mip levels only when your real path uses them, then compare the model with observed resource lifetimes. Do not add the encoded PNG bytes to this GPU subtotal; account for retained network buffers in the CPU working-set model separately.</p>
<p>Loading all scenes in advance changes the working set even if each scene is individually small. Organize atlases around assets that must coexist, while considering the effects on batching and loading. A smaller file is not automatically a better split if it creates many live copies.</p>
<h2>Test release behavior and capacity separately</h2>
<p>On each target device, record a cold start, repeated scene entry, scene exit and re-entry. Use the same content and viewport. Confirm that resources intended to be released are no longer referenced through caches, previews or old scene objects. A stable post-transition plateau is more informative than one screenshot of a memory counter.</p>
<p>The <a href="https://www.framesprite.com/guides/sprite-imagebitmap-graphics-resource-close-budget">Reference: ImageBitmap lifecycle guide</a> covers releasing review-image resources. The <a href="https://www.framesprite.com/guides/unity-addressables-atlas-duplicate-build-dependency-audit">Addressables atlas audit</a> addresses a different source of duplication in Unity deliveries. Neither is a substitute for measuring your runtime's allocations.</p>
<p>Also verify <a href="https://www.framesprite.com/guides/webgl-sprite-atlas-max-texture-size-context">the current WebGL context's texture-size limit</a>. A dimension limit is a capability constraint, not a promise that many maximum-size textures will fit in memory.</p>
<p>The <a href="https://www.framesprite.com/guides/mobile-sprite-texture-memory-budget">mobile texture-memory reference</a> covers the broader budget. Keep this transition worksheet's assumptions with the test results. If the chosen budget cannot accommodate the peak, evaluate the approved fallback, reduce simultaneous assets, or change release sequencing and measure the resulting load behavior. Approve an atlas plan only after the worst supported transition leaves headroom for the rest of the application.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering prepared this AI-assisted article using primary graphics documentation and an explicit synthetic allocation model. The team has a commercial interest in FrameSprite. No device memory benchmark is claimed. <a href="https://www.framesprite.com/about#workflow-team">Editorial ownership and contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Approve Sprite Timing Without Trusting the GIF]]></title><description><![CDATA[Build a timing review packet that separates a convenient GIF preview from source frame durations, engine playback and delivery acceptance.]]></description><link>https://framesprite.hashnode.dev/gif-preview-game-timing-evidence</link><guid isPermaLink="true">https://framesprite.hashnode.dev/gif-preview-game-timing-evidence</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:19:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/afac657f-dc8a-4512-a239-3345ae5737de.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Approve a sprite animation against its ordered frame IDs and intended hold durations, then compare the target runtime with that record. Use a GIF as a convenient visual preview. Its appearance alone cannot prove that the exported metadata or the game's playback preserves the same timing.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Review pose order, per-pose hold and cycle length separately.</li>
<li>Keep the delivery manifest alongside the preview.</li>
<li>Capture the engine's observations before blaming the art or the GIF viewer.</li>
</ul>
</blockquote>
<p>An artist and developer can agree that an attack “feels too fast” while examining different artifacts. One person watches a preview with a long anticipation hold. Another imports four images at a single frame rate. Neither frame count nor a matching thumbnail reveals that disagreement.</p>
<h2>Give the handoff one timing authority</h2>
<p>Decide which file or manifest defines the intended timing. Record its units and revision. Aseprite supports <a href="https://www.aseprite.org/docs/sprite-sheet/">sprite-sheet export with frame and tag selection</a>, while its <a href="https://www.aseprite.org/api/frame/">Frame API</a> expresses a frame's duration in seconds. Those facts do not make every importer consume that information automatically.</p>
<p>Avoid reconstructing the production timing by eye from an animated image. Keep source frames, animation names, ordered frame IDs, duration values and loop policy together. If your engine can only use equal-length steps, document the conversion rather than silently treating every pose as one equal step.</p>
<p>The review record should also state whether an impact event belongs to entering a pose, reaching a timestamp, or game logic. A visible contact pose and an engine event are different observations. The manifest can specify the intended event time without claiming that a screenshot proves the event fired.</p>
<h2>Use a four-pose fixture with a visible hold</h2>
<p>This synthetic example has four distinct images. Its deliberately long anticipation makes a lost hold easy to spot.</p>
<table>
<thead>
<tr>
<th>Frame ID</th>
<th>Meaning</th>
<th align="right">Duration, ms</th>
<th>Interval within one cycle, ms</th>
</tr>
</thead>
<tbody><tr>
<td>windup</td>
<td>Weapon moves back</td>
<td align="right">60</td>
<td>0 to less than 60</td>
</tr>
<tr>
<td>hold</td>
<td>Anticipation pose</td>
<td align="right">180</td>
<td>60 to less than 240</td>
</tr>
<tr>
<td>strike</td>
<td>Contact pose</td>
<td align="right">60</td>
<td>240 to less than 300</td>
</tr>
<tr>
<td>recover</td>
<td>Return pose</td>
<td align="right">100</td>
<td>300 to less than 400</td>
</tr>
</tbody></table>
<p>The intended cycle is 400 ms. A four-frame importer set to 10 frames per second also produces a 400 ms cycle, but assigns 100 ms to every pose. Matching total length therefore does not prove that the hold or strike time survived.</p>
<p>For an offline arithmetic check, the following Python example defines half-open intervals: a timestamp exactly at a boundary belongs to the next pose. It is an illustrative timing oracle, not an engine implementation or a benchmark.</p>
<pre><code class="language-python">frames = [('windup', 60), ('hold', 180), ('strike', 60), ('recover', 100)]
def pose_at(milliseconds):
    remaining = milliseconds % sum(duration for _, duration in frames)
    for name, duration in frames:
        if remaining &lt; duration:
            return name
        remaining -= duration
assert [pose_at(t) for t in [0, 60, 239, 240, 300, 400]] == [
    'windup', 'hold', 'hold', 'strike', 'recover', 'windup']
</code></pre>
<p>Use distinct pose labels rather than nearly identical art for the first test. Once the importer passes, substitute the actual frames while preserving the record.</p>
<h2>Inspect the preview without promoting it to ground truth</h2>
<p>For a GIF export, inspect the written frame delays and reconstructed frames. <a href="https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#gif">Pillow's GIF documentation</a> describes duration and disposal controls. Pick disposal behavior for the actual frame content; one hard-coded disposal setting is not a universal repair for every animation.</p>
<p>A preview may include palette conversion, transparency handling or partial-frame composition. If it shows trails, determine whether the decoded composed frames are wrong before changing the original poses. If the display looks fast, compare its recorded delays with the manifest first. Do not infer a universal minimum GIF delay from one viewer's behavior.</p>
<p>Keep a still contact sheet with frame IDs beside the animation. It catches order and omission errors even when the animated preview is difficult to pause. For deeper image-processing checks, see <a href="https://www.framesprite.com/guides/pillow-gif-seek-mode-normalization">consistent GIF frame modes</a> and <a href="https://www.framesprite.com/guides/pillow-sequence-iterator-independent-frame-copies">independent copies during sequence iteration</a>.</p>
<h2>Require the runtime to answer the same questions</h2>
<p>Import the fixture into a disposable scene. Disable transitions, random starting offsets and unrelated speed multipliers. Record the imported frame sequence, duration interpretation, loop setting and any global time scale. Then observe pose entry times over several cycles.</p>
<p>Compare intended intervals with runtime observations using a stated tolerance appropriate to its update and measurement clocks. Do not require a rendered frame at every mathematical timestamp: a display presents discrete samples. Require the correct order, preserved long hold and a documented explanation for the measurement tolerance.</p>
<p>If a recording drops or resamples frames, it becomes another transform of the evidence. The <a href="https://www.framesprite.com/guides/sprite-flash-review-capture-resampling">capture-resampling guide</a> explains why a missing image in a capture does not by itself prove a missing source pose.</p>
<p>The acceptance packet should contain the revision, manifest, contact sheet, preview, runtime settings and observed timings. A pass applies to that import path and configuration; gameplay transitions and load spikes need their own checks.</p>
<h2>Evidence and editorial responsibility</h2>
<p>FrameSprite Engineering prepared this AI-assisted explanation from the linked primary documentation and a synthetic timing fixture. The team has a commercial interest in FrameSprite. No engine or viewer performance result is claimed. <a href="https://www.framesprite.com/about#workflow-team">Editorial ownership and contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Sprite Visible, Canvas Export Blocked? Test CORS]]></title><description><![CDATA[Separate image display, canvas readback and Blob creation when a sprite preview looks correct but its browser export fails.]]></description><link>https://framesprite.hashnode.dev/browser-sprite-cors-export-check</link><guid isPermaLink="true">https://framesprite.hashnode.dev/browser-sprite-cors-export-check</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 30 Oct 2026 01:15:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb1b5b7a096292d05562d8/1414d6cb-5baa-4fad-b92a-b0cb8fa1c4c4.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A sprite can be visible in a browser while remaining unavailable for canvas export. Test three separate outcomes: the image loads, a fresh canvas can read its pixels, and the encoder returns a non-null Blob. Fix the first failed stage instead of repeatedly changing the PNG or renaming a failed download.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Image display does not prove permission to read or export pixels.</li>
<li>Configure the image request and the image server together.</li>
<li>Keep a same-origin control and test each input before composing the final sheet.</li>
</ul>
</blockquote>
<p>This walkthrough is for developers who control a browser-based sprite reviewer or exporter. Use a disposable page on your normal development origin and a small image you own. Never test against private images or try to disable the browser's security checks.</p>
<h2>Locate the first failed boundary</h2>
<p>Write down the page origin and the final image URL, including redirects. Then give each observation its own result column. An HTTP success in DevTools is useful network evidence; it is not an export result.</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Passing observation</th>
<th>A failure directs you toward</th>
</tr>
</thead>
<tbody><tr>
<td>Image load</td>
<td>Load completes with nonzero intrinsic dimensions</td>
<td>URL, server response, decoding or request policy</td>
</tr>
<tr>
<td>Pixel readback</td>
<td>Reading one pixel succeeds on a fresh canvas</td>
<td>Origin-clean permission for the drawn inputs</td>
</tr>
<tr>
<td>Encoding</td>
<td>Callback receives a non-null Blob</td>
<td>Encoder failure, invalid canvas or export handling</td>
</tr>
<tr>
<td>Download</td>
<td>Saved bytes have the intended format</td>
<td>Filename, MIME and download code</td>
</tr>
</tbody></table>
<p>The <a href="https://html.spec.whatwg.org/multipage/canvas.html#security-with-canvas-elements">HTML canvas security model</a> separates displaying foreign content from reading it back. Drawing an input without the required permission can contaminate a larger composition too. A clean background does not cancel a problematic foreground.</p>
<h2>Make a fresh, permission-aware request</h2>
<p>The following browser example uses anonymous CORS, sets the request mode before assigning the URL, and treats load and encoding errors separately. It is an illustrative implementation, not a report of testing your CDN or browser combination.</p>
<pre><code class="language-javascript">async function spritePng(url, maxPixels = 8_388_608) {
  if (!Number.isSafeInteger(maxPixels) || maxPixels &lt; 1) {
    throw new Error('Invalid export pixel budget');
  }
  const image = new Image();
  image.crossOrigin = 'anonymous';
  await new Promise((resolve, reject) =&gt; {
    image.onload = resolve;
    image.onerror = () =&gt; reject(new Error('Image request or decode failed'));
    image.src = url;
  });
  if (!image.naturalWidth || !image.naturalHeight) {
    throw new Error('Image has no decoded dimensions');
  }
  if (image.naturalWidth &gt; maxPixels / image.naturalHeight) {
    throw new Error('Image exceeds the configured export pixel budget');
  }
  const canvas = document.createElement('canvas');
  canvas.width = image.naturalWidth;
  canvas.height = image.naturalHeight;
  const context = canvas.getContext('2d');
  if (!context) throw new Error('Canvas 2D unavailable');
  context.drawImage(image, 0, 0);
  context.getImageData(0, 0, 1, 1); // May throw SecurityError.
  return await new Promise((resolve, reject) =&gt; {
    try {
      canvas.toBlob(blob =&gt; blob ? resolve(blob) :
        reject(new Error('PNG encoder returned null')), 'image/png');
    } catch (error) { reject(error); }
  });
}
</code></pre>
<p>The pixel cap is an example application policy, not a browser capacity claim. Set it from your own resource budget; the image may already consume decode memory before this canvas-allocation check. A production loader also needs cancellation and a request deadline. The server still has to approve the request. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image">MDN's cross-origin canvas guide</a> explains that the image's <code>crossorigin</code> setting works with the response's CORS headers. A client attribute alone cannot grant access. Anonymous mode also is not a substitute for designing an authenticated image-delivery flow.</p>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob"><code>toBlob()</code> reference</a> documents both a possible null callback value and a <code>SecurityError</code> for a canvas that is not origin-clean. Preserve those distinctions in your error message. “Export failed” hides the information needed to repair the request.</p>
<h2>Run a two-origin, four-case acceptance test</h2>
<p>Prepare identical small artwork at three controlled URLs: same origin, another origin with CORS approval, and another origin without it. For the approved anonymous case, the remote server can return <code>Access-Control-Allow-Origin</code> matching the page origin, or <code>*</code> for a public, non-credentialed image. Keep authenticated image handling separate. Use a new image and canvas for every case.</p>
<table>
<thead>
<tr>
<th>Controlled case</th>
<th>Image request</th>
<th>Response CORS approval</th>
<th>Expected load</th>
<th>Expected fresh-canvas export</th>
</tr>
</thead>
<tbody><tr>
<td>Same origin</td>
<td>Ordinary image</td>
<td>Not needed for this same-origin control</td>
<td>Succeeds</td>
<td>Succeeds</td>
</tr>
<tr>
<td>Other origin</td>
<td>Ordinary image, no <code>crossOrigin</code></td>
<td>Absent</td>
<td>Can succeed</td>
<td><code>SecurityError</code> after drawing</td>
</tr>
<tr>
<td>Other origin</td>
<td><code>crossOrigin = 'anonymous'</code></td>
<td>Absent</td>
<td>Fails CORS approval</td>
<td>Not attempted</td>
</tr>
<tr>
<td>Other origin</td>
<td><code>crossOrigin = 'anonymous'</code></td>
<td>Permits this page</td>
<td>Succeeds</td>
<td>Succeeds</td>
</tr>
</tbody></table>
<p>The table assumes valid image bytes, accessible URLs and no separate content-security policy restriction. A network or decoding failure can prevent any case from reaching its intended permission check.</p>
<p>For the same-origin control, require a non-null PNG Blob and a decoded saved image of the original dimensions. For the approved remote URL, require the same result. With this anonymous-CORS loader, the unapproved remote URL should fail to load; record that as a request-policy failure. A separate plain image element might display that URL, but drawing that plain image to canvas does not establish readback permission.</p>
<p>Next, compose the intended atlas one input at a time. If every isolated input passes but the complete atlas fails, inspect additional overlays, fonts-as-raster inputs and copied canvases used only by the composition path. Keep the first failing input's URL in the diagnostic, with credentials and sensitive query parameters removed.</p>
<h2>Repair the responsible layer</h2>
<p>For an owned CDN, repair its image response policy and repeat the approved-remote test. For a third-party host that does not grant pixel access, request a permitted download or let the user select a local file they have permission to use. Do not route arbitrary private URLs through a new proxy.</p>
<p>Changing <code>fetch()</code> to <code>no-cors</code> does not produce readable image bytes; see the separate <a href="https://www.framesprite.com/guides/sprite-no-cors-opaque-zero-byte-blob">opaque-response and empty-Blob explanation</a>. If readback works but the saved extension is wrong, follow the <a href="https://www.framesprite.com/guides/canvas-export-mime-fallback-file-extension">encoder MIME check</a>. If the file is correct but its name cannot be read from a response header, inspect <a href="https://www.framesprite.com/guides/sprite-content-disposition-cors-header-exposure">CORS header exposure</a>.</p>
<p>The <a href="https://www.framesprite.com/guides/canvas-sprite-export-cors-tainted-canvas">existing tainted-canvas reference</a> explains the underlying rule. This matrix adds a reproducible comparison of request mode and response approval. Passing it proves the tested request and export path worked for those inputs. It does not prove every browser, redirected URL, signed-link lifetime or production policy is equivalent.</p>
<h2>Evidence and editorial responsibility</h2>
<p>Prepared by FrameSprite Engineering using primary browser documentation and an illustrative diagnostic procedure. This is AI-assisted editorial content from a team with a commercial interest in FrameSprite. No production CDN or cross-browser test is claimed. <a href="https://www.framesprite.com/about#workflow-team">Editorial ownership and contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Phaser 3 Idle Stuck on Frame One? Stop Restarting play]]></title><description><![CDATA[Diagnose repeated Phaser 3.90 animation restarts with a bounded 60-step control, ignoreIfPlaying, and separate one-shot action policies.]]></description><link>https://framesprite.hashnode.dev/phaser-3-idle-stuck-on-frame-one-stop-restarting-play</link><guid isPermaLink="true">https://framesprite.hashnode.dev/phaser-3-idle-stuck-on-frame-one-stop-restarting-play</guid><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Thu, 29 Oct 2026 18:06:50 GMT</pubDate><content:encoded><![CDATA[<p><em>Publisher disclosure: AI-assisted writing by FrameSprite Engineering, the commercial operator of the original guide site. Local controls are executed only within their stated scope.</em></p>
<p><a href="https://www.framesprite.com/guides/phaser390-update-play-ignore-if-playing">Original article and evidence</a></p>
<p>If <code>update()</code> calls <code>sprite.anims.play('idle')</code> every frame, inspect whether each call restarts the animation. In Phaser 3.90, passing <code>true</code> as <code>ignoreIfPlaying</code> lets an already playing animation with the same key continue. The <a href="https://docs.phaser.io/api-documentation/3.90.0/class/animations-animationstate#play">3.90 AnimationState.play reference</a> defines the option; the <a href="https://raw.githubusercontent.com/phaserjs/phaser/v3.90.0/src/animations/AnimationState.js">pinned implementation</a> checks both the playing state and current animation key.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>A correct spritesheet can appear frozen because playback keeps restarting.</li>
<li>Ignore repeated requests for an ongoing loop, or play when its owning state changes.</li>
<li>Handle deliberate one-shot retriggers separately from idle and run loops.</li>
</ul>
</blockquote>
<h2>Establish that the animation itself is valid</h2>
<p>Play the same key once from scene creation in a disposable scene. Check that the texture, frame range, timing and repeat settings produce the intended sequence. If one initial play works but a per-frame call appears stuck, inspect repeated playback requests before reslicing the artwork.</p>
<p>Our original local control reproduced a bounded version of this failure with the verified Phaser 3.90.0 library, three synthetic texture frames, a four-FPS repeating animation and the Canvas renderer. Automatic game-loop execution was stopped. Each of two sprites received sixty same-key play requests, with an explicit 20 ms AnimationState update after each request.</p>
<p>The unguarded sprite produced sixty animation-start events and every recorded frame index was 1. With ignoreIfPlaying enabled, there was one start event and the trace visited indices 1, 2 and 3. The <a href="https://www.framesprite.com/guides/phaser-play-controls.json">complete animation-state trace</a> records all sixty steps per sprite, the library hash and browser version. This demonstrates the controlled state-reset distinction; it is not a real-time gameplay, hardware timing, performance or all-version test.</p>
<p>For loading and creating the animation, use the <a href="https://www.framesprite.com/guides/phaser-3-sprite-sheet-animation">Phaser spritesheet setup guide</a>. This diagnosis begins after that definition works and focuses on preserving playback state.</p>
<h2>Ignore the same animation while it is playing</h2>
<p>For an idle or run loop selected every update, the intended key can be requested without resetting that same ongoing animation:</p>
<pre><code class="language-javascript">// Illustrative Phaser 3.90 update logic.
const desiredKey = isMoving ? 'hero-run' : 'hero-idle';
player.anims.play(desiredKey, true);
</code></pre>
<p>The second argument does not freeze the sprite on its previous animation forever. The pinned implementation compares the requested animation key with the currently playing key. A request for a different key can still start the new animation.</p>
<p>Do not generalize this into “play is harmless everywhere.” Several systems can still request different keys in the same frame. Log the requester and desired key if locomotion, combat and presentation controllers all write to the animation state. Give one controller ownership of the final selection rather than allowing last-writer order to decide accidentally.</p>
<h2>Choose a state-entry policy for one-shot actions</h2>
<p>A state-driven controller can call play once when entering idle, running or attacking. That makes transitions explicit and avoids per-frame calls entirely. Keep the distinction between entering the same logical state again and remaining in that state.</p>
<p><code>ignoreIfPlaying</code> is not a complete one-shot action guard. Once a non-looping action finishes, a later update can start it again if the surrounding controller still requests that key. Conversely, an intentional second attack may need a deliberate restart even while the first animation is playing. Decide that behavior using action state and input rules, not by adding true everywhere.</p>
<p>Track the action identity when callbacks apply consequences. A correct animation selection does not authorize repeated damage or rewards. The <a href="https://www.framesprite.com/guides/phaser-scene-restart-animation-listener-lifecycle">scene listener-lifetime guide</a> covers a different source of duplicate effects: repeated subscriptions across scene runs.</p>
<h2>Distinguish restarts from stalled time</h2>
<p>If the current frame still does not advance, inspect paused animation state, scene activity, timing values and the actual key selected. A disabled update path or invalid frame definition is not repaired by ignoreIfPlaying. Likewise, a loop with only one real frame can legitimately remain visually unchanged.</p>
<p>Use counters sparingly: record play requests and animation-start events with the sprite's identity. Do not infer every repeated event is a playback restart without checking whether several sprites emitted it. Keep the probe local rather than logging every frame indefinitely in production.</p>
<h2>Verify the controller, not only one screenshot</h2>
<p>Prepare distinct idle and run loops plus a one-shot attack. The broader controller controls below are proposed, not executed. The local same-key animation-state control above is narrower and does not verify scene transitions, input or combat behavior.</p>
<table>
<thead>
<tr>
<th>Control</th>
<th>Acceptance</th>
</tr>
</thead>
<tbody><tr>
<td>Repeated same-key loop request</td>
<td>Frame sequence advances without repeated reset</td>
</tr>
<tr>
<td>Idle to run transition</td>
<td>New key begins under the selected state policy</td>
</tr>
<tr>
<td>Attack held after completion</td>
<td>No unintended perpetual retrigger</td>
</tr>
<tr>
<td>Deliberate second attack</td>
<td>Restart or queue behavior matches the action rule</td>
</tr>
<tr>
<td>Scene restart</td>
<td>One current controller and intended listener count</td>
</tr>
</tbody></table>
<p>Record Phaser version, selected keys, repeat settings and observed start events. Retain a negative control using repeated unguarded play so future controller changes can be compared against the known failure mechanism. Keep the playback policy with the <a href="https://www.framesprite.com/guides/sprite-animation-handoff-checklist">animation handoff</a>.</p>
<h2>Editorial responsibility</h2>
<p>Prepared with AI assistance by the site's engineering editorial team. Definitions are pinned to Phaser 3.90. The isolated same-key animation-state control was executed; application snippets and the broader gameplay controls were not. No demand-frequency claim is made.  See the <a href="https://www.framesprite.com/about#workflow-team">editorial policy</a> and <a href="https://www.framesprite.com/contact">corrections contact</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Pillow Autocontrast Mask Still Changes Outside the Selection]]></title><description><![CDATA[Pillow autocontrast masks select histogram samples. Use a separate write mask to preserve pixels outside the selection, with reproducible checks.]]></description><link>https://framesprite.hashnode.dev/pillow-autocontrast-mask-still-changes-outside-the-selection</link><guid isPermaLink="true">https://framesprite.hashnode.dev/pillow-autocontrast-mask-still-changes-outside-the-selection</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 23 Oct 2026 09:03:30 GMT</pubDate><content:encoded><![CDATA[<p>Disclosure: This article was generated with AI for the FrameSprite team, which has a commercial interest in FrameSprite. It received a separate AI editorial review against the cited sources. Executed synthetic fixtures are identified with their runtime and limits; no other editor, engine, production or performance test is implied.</p>
<p>Use a second compositing step if only a selected region may receive Pillow's autocontrast result. The <code>mask</code> argument to <code>ImageOps.autocontrast</code> selects pixels for the histogram calculation. It does not restrict where the resulting tonal lookup table is applied. Calculate the adjustment using a statistical mask, then write it through a separate output mask.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Name the histogram-selection mask separately from the allowed-write mask.</li>
<li>Composite the adjusted result over the original to protect outside pixels.</li>
<li>Check both that the selection changed and that the protected region did not.</li>
</ul>
</blockquote>
<h2>Why the untouched border changes</h2>
<p>Suppose an opaque grayscale strip contains <code>[20, 60, 100, 140, 180, 220]</code>. You want to increase contrast only in the middle pair, without altering the surrounding reference marks. A binary mask <code>[0, 0, 255, 255, 0, 0]</code> selects the samples 100 and 140.</p>
<p>The <a href="https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.autocontrast">autocontrast reference</a> states that the histogram is computed using pixels within the mask. The <a href="https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageOps.py">maintained implementation</a> calculates that masked histogram, builds the lookup table, and applies it to the image. A histogram boundary is therefore different from an output boundary.</p>
<p>With the fixture's default zero cutoff and <code>preserve_tone=False</code>, the selected endpoints map to 0 and 255. Applying that mapping to the full strip produced <code>[0, 0, 0, 255, 255, 255]</code>. All four outside samples changed, even though none contributed to the histogram.</p>
<p><img src="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture.webp" alt="Executed grayscale strip before adjustment, after histogram-mask-only adjustment, and after protecting the outside region with a write mask" /></p>
<p>The general protected-region requirement also appears in the <a href="https://www.framesprite.com/guides/ai-character-outfit-edit-mask">local outfit-edit workflow</a>. This task has a different cause: a deterministic histogram API uses its mask for statistics. No generated replacement image or character-edit interface is involved.</p>
<h2>Calculate first, constrain the write second</h2>
<p>Use two names even when both masks currently contain the same binary pixels:</p>
<pre><code class="language-python">from PIL import Image, ImageOps

original = Image.new("L", (6, 1))
original.putdata([20, 60, 100, 140, 180, 220])

statistics_mask = Image.new("L", original.size)
statistics_mask.putdata([0, 0, 255, 255, 0, 0])
write_mask = statistics_mask.copy()

adjusted = ImageOps.autocontrast(original, mask=statistics_mask)
local = Image.composite(adjusted, original, write_mask)
local.save("pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png", format="PNG")

with Image.open("pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png") as delivered:
    delivered.load()
    assert delivered.mode == "L"
    assert list(delivered.get_flattened_data()) == [20, 60, 0, 255, 180, 220]
</code></pre>
<p>The <a href="https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.composite">composite reference</a> explains the second operation: the mask combines the first and second images. Here white selects <code>adjusted</code>, while black selects <code>original</code>. Image sizes and the compositing mask must agree. This example uses <code>L</code> images and a binary <code>L</code> mask throughout; it is not an RGBA source-over or transparency tutorial.</p>
<p>The masks can later differ on purpose. You might derive contrast from a representative interior patch but permit the adjustment over a larger repair region. Keep that decision explicit. This fixture does not test feathered masks; introducing intermediate write-mask values creates a blended transition rather than exact selection of one input at every pixel.</p>
<h2>Test protection and useful change together</h2>
<p>An unchanged protected region is necessary, but returning the original image everywhere would also satisfy that condition. Verify the selected region as well:</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Executed result</th>
</tr>
</thead>
<tbody><tr>
<td>Histogram-only changes outside indices</td>
<td><code>0, 1, 4, 5</code></td>
</tr>
<tr>
<td>Constrained output changes outside indices</td>
<td>None</td>
</tr>
<tr>
<td>Selected samples after constrained output</td>
<td><code>0, 255</code></td>
</tr>
<tr>
<td>Selected samples actually changed</td>
<td>Both</td>
</tr>
</tbody></table>
<p>All nine checks passed on Python 3.12.10 and Pillow 13.0.0 on Windows. The script saved and reopened the constrained PNG and verified exact decoded samples. Its empty statistical-mask and constant statistical-region controls both returned the unchanged strip in this version; they do not demonstrate a useful contrast increase. For production work, reject or report an empty selection instead of counting it as an editing success.</p>
<p>Compare decoded output pixels when testing protection. A different compressed file hash alone does not identify a changed region; see the separate <a href="https://www.framesprite.com/guides/sprite-png-file-hash-versus-decoded-pixel-identity">file-hash versus decoded-pixel identity guide</a>. If your later pipeline adds transparency, perform its separate <a href="https://www.framesprite.com/guides/sprite-animation-transparent-pixel-rgb-qa">transparent-pixel RGB review</a>; this opaque grayscale test makes no alpha-preservation claim.</p>
<h2>Retain the reproducible repair</h2>
<p>Download the <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-contrast-source.png">original strip</a>, <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-histogram-mask-only.png">histogram-only result</a>, <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png">constrained result</a>, <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture-input.json">input masks and settings</a>, and <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture-results.json">execution record</a>. The independent <a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-supplement-fixtures.py">fixture script</a> runs only this bounded case with:</p>
<pre><code class="language-sh">python pillow-autocontrast-statistics-mask-output-region-supplement-fixtures.py --out-root ./reproduced --only pillow-autocontrast-statistics-mask-output-region
</code></pre>
<p>The evidence establishes statistical selection versus write selection for these synthetic <code>L</code> pixels. It does not establish seam quality in a painted image, production compatibility, or results in a GUI editor.</p>
<p>Prepared by the <a href="https://www.framesprite.com/about#workflow-team">FrameSprite Workflow Team</a>, which has a commercial relationship with this site. AI-assisted technical writing using original synthetic data, official API references, and an executed export; no generative editor or GUI was tested. <a href="https://www.framesprite.com/about">Editorial method</a> · <a href="https://www.framesprite.com/contact">Corrections</a>.</p>
<p><a href="https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region">Original guide and downloadable materials</a></p>
]]></content:encoded></item><item><title><![CDATA[Why do faded SVG character parts form dark overlap seams?]]></title><description><![CDATA[Fix dark SVG overlap seams by fading a composited group. Compare exact SVG structures, alpha calculations and a bounded Chromium fixture.]]></description><link>https://framesprite.hashnode.dev/why-do-faded-svg-character-parts-form-dark-overlap-seams</link><guid isPermaLink="true">https://framesprite.hashnode.dev/why-do-faded-svg-character-parts-form-dark-overlap-seams</guid><category><![CDATA[SVG]]></category><dc:creator><![CDATA[FrameSprite]]></dc:creator><pubDate>Fri, 23 Oct 2026 08:40:44 GMT</pubDate><content:encoded><![CDATA[<p>Disclosure: This article was generated with AI for the FrameSprite team, which has a commercial interest in FrameSprite. It received a separate AI editorial review against the cited sources. Executed synthetic fixtures are identified with their runtime and limits; no other editor, engine, production or performance test is implied.</p>
<p>Put the fade on a group when overlapping pieces should behave as one translucent character. Giving every child opacity 0.5 composites several partly transparent objects over one another. A group opacity of 0.5 instead applies after the group's children have formed their combined result. Their overlap can therefore look different even when every listed opacity value appears to be the same.</p>
<blockquote>
<p><strong>Key Takeaways</strong></p>
<ul>
<li>Parent opacity and repeated child opacity have different coverage.</li>
<li>Choose whether the fade belongs to a whole asset or independent components.</li>
<li>Check overlaps as well as isolated edges during acceptance.</li>
</ul>
</blockquote>
<h2>Establish which object is supposed to fade</h2>
<p>An assembled character badge may contain a torso, sleeve, weapon, and shadow. If those parts form one icon, fading each part independently can reveal overlap seams that the opaque version concealed. If each part intentionally represents a separate translucent object, that same overlap may be correct.</p>
<p>The <a href="https://www.w3.org/TR/SVG2/render.html#ObjectAndGroupOpacityProperties">SVG rendering model's group-opacity section</a> describes the distinction. The <a href="https://www.w3.org/TR/compositing-1/#simplealphacompositing">Compositing and Blending specification</a> supplies the source-over coverage calculation. This is about compositing order, not a different PNG palette or a newly generated shadow.</p>
<p>Use <a href="https://www.framesprite.com/guides/sprite-equipment-front-back-occlusion-track">equipment occlusion-order tracking</a> for the separate question of which part appears in front. Correct drawing order does not make child fades equivalent to group coverage.</p>
<h2>Compare two explicit SVG structures</h2>
<pre><code class="language-xml">&lt;!-- Whole character fades after its pieces have combined. --&gt;
&lt;g opacity="0.5"&gt;
  &lt;rect x="35" y="30" width="85" height="60" fill="#126c58"/&gt;
  &lt;rect x="85" y="30" width="80" height="60" fill="#126c58"/&gt;
&lt;/g&gt;
</code></pre>
<p>For the control, remove opacity from the group and put <code>opacity="0.5"</code> on each rectangle. Keep their geometry, color, and order unchanged. Do not compare this with changing <code>fill-opacity</code> and stroke settings simultaneously, because that introduces another variable.</p>
<p>With two identical half-alpha objects over transparent content, the source-over alpha calculation is <code>0.5 + 0.5 × (1 - 0.5) = 0.75</code> in the overlap. For the opaque children combined first and then faded as one group, coverage is 0.5. The accompanying Python check verifies this arithmetic; it does not measure a rendered screenshot.</p>
<img src="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-before-render.webp" alt="Before: actual Chromium render of the original synthetic SVG control" style="display:block;margin:0 auto" />

<img src="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-after-render.webp" alt="After: actual Chromium render of the changed synthetic SVG input" style="display:block;margin:0 auto" />

<p>In the recorded Chromium 156.0.8078.4 run, the two 402 by 243 RGB screenshot crops differed at 31,200 pixel positions. The child-opacity input shows a darker overlap band; the grouped input removes that band for these identical opaque shapes. A nonzero difference confirms that these inputs rendered differently; it is not a quality score or a universal repair-success measure. The <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-browser-fixture-results.json">browser result record</a> binds the screenshots to the source files. This covers one installed Chromium renderer, not an editor round trip or cross-browser certification.</p>
<h2>Keep blend modes and isolation visible</h2>
<p>Special blend modes, masks, filters, and group isolation can add other dependencies. Start with normal source-over and ordinary opaque fills, then reintroduce the required effects one at a time. Do not flatten the only editable source merely to hide a seam.</p>
<p>The <a href="https://www.framesprite.com/guides/aseprite-rgba-layer-blend-flatten-export-contract">layer-blend export guide</a> handles preserving an editor composite. Here the practical result is an explicit SVG hierarchy that says where the fade occurs. The <a href="https://www.w3.org/TR/SVG2/struct.html#Groups">SVG document-structure specification</a> defines the grouping element used by that hierarchy.</p>
<h2>Review the actual consumer</h2>
<p>Render both minimal SVGs in the intended browser or raster exporter. Inspect an isolated region and the overlap over a fixed background. If opacity behaves correctly in the standalone SVG but changes after conversion or bundling, compare the exported structure rather than moving color values until it looks similar.</p>
<p>Keep a transparent source, a declared review background, and the final raster together in the <a href="https://www.framesprite.com/guides/sprite-animation-handoff-checklist">asset handoff</a>. A white screenshot alone cannot show whether the exported file contains the intended coverage.</p>
<p>The <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-fixture-browser.html">before/after comparison page</a>, <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-fixture-input.json">input</a>, <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-fixture-results.json">result record</a>, and <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-art-fixtures.py">fixture script</a> preserve the synthetic overlap example. Their geometry/arithmetic scope does not certify every SVG renderer or game texture pipeline.</p>
<h2>Reproduce the retained structure check</h2>
<p>The retained Python check and the recorded Chromium screenshots are separate evidence. Use the recorded Python 3.12.10 / Pillow 13.0.0 environment when comparing these results; other versions have not been certified. Run the downloaded checker for this case in a fresh output directory:</p>
<pre><code class="language-sh">python svg-group-opacity-overlap-seams-art-fixtures.py --out-root ./verification --only svg-group-opacity-overlap-seams
</code></pre>
<p>The checker creates its case subfolder under <code>verification</code>; it checks declared SVG structure and constructs the synthetic inputs. It does not reproduce an editor round trip or execute the separate Chromium screenshot pass. To inspect the retained browser pair locally, download the <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-before.svg">before SVG</a>, <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-after.svg">after SVG</a>, and <a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams-fixture-browser.html">comparison HTML</a> into one folder without renaming them, then open the comparison page. Its dependencies use these exact case-specific filenames.</p>
<p>Prepared by the <a href="https://www.framesprite.com/about#workflow-team">FrameSprite Workflow Team</a>. AI-assisted technical writing with primary compositing definitions and an explicit synthetic coverage calculation. <a href="https://www.framesprite.com/about">Editorial method</a> · <a href="https://www.framesprite.com/contact">Corrections</a>.</p>
<p><a href="https://www.framesprite.com/guides/svg-group-opacity-overlap-seams">Original guide and downloadable materials</a></p>
]]></content:encoded></item></channel></rss>