feat(core): placeholderColor — solid color placeholder while a texture loads#96
Merged
Conversation
A node with src + color tints the image (color is a vertex attribute multiplied into the texture sample), and a node with an unloaded texture renders nothing — so there was no single-node way to show a background placeholder while an image loads. New per-node placeholderColor prop (default 0 = off): while the texture is not loaded the quad samples the stage's shared 1x1 white default texture tinted by the premultiplied placeholder color — exactly the color-rect path, through the node's own shader, so rounded corners and borders apply. The placeholder also shows while a memory-pressure-freed texture reloads and remains if the texture permanently fails. Cost model: zero extra nodes/quads/overdraw; placeholder state is recomputed only from texture lifecycle events (nothing added to the per-translation scroll path); shader uniform value-key caching is untouched (uniforms remain a function of resolvedProps + w/h); all placeholder quads share one texture so they batch at least as well as loaded images. Implemented for both WebGL and Canvas2D. Also fixes animation color detection to match camelCase *Color props so animating placeholderColor interpolates in color space. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
placeholderColor is not animatable, so the broader isColor match is unnecessary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New per-node
placeholderColorprop: a node with a texture renders a solid rect of this color until the texture loads, instead of rendering nothing. The placeholder renders through the node's own shader, soRounded/RoundedWithBordercorners apply to it. It also shows while a memory-pressure-freed texture reloads (the #87/#95 lifecycle) and remains if the texture permanently fails. Default0= off, so existing apps are unaffected.Why
coloris a per-vertex attribute multiplied into the texture sample, so setting it withsrctints the image — and an unloaded texture renders nothing (isRenderable = textureLoaded). There was no single-node way to express "rect now, image when loaded". The two-node workaround costs an extra quad of rounded-shader fragment work per card plus event boilerplate.How
CoreNode.placeholderActive— true iffplaceholderColor !== 0, texture set, texture not loaded. Recomputed by one private helper from exactly the code paths where its inputs change (texture setter,loaded/failed/freedhandlers, placeholderColor setter). Toggles raisePremultipliedColors | IsRenderable, processed in the same frame's update pass.placeholderColor; both renderers substitute the stage's shared 1×1 whitedefaultTexturefor the quad (one boolean check per quad);updateIsRenderable()treats the node as renderable, including the exhausted-retry branch; therenderTexturegetter substitution also fixes the RTT loaded-state guard.TV cost model (why this shape)
resolvedProps+w/h; the placeholder→image swap changes neither, so shared uniform collections and the bindRenderOp reference-equality skip (perf(webgl): skip redundant uniform uploads in bindRenderOp #92) survive the transition.Testing
CoreNode.test.tsdriving the realloaded/freed/failedevent chain via an EventEmitter-backed texture fake: loading shows the placeholder with correct premultiplied colors; loaded switches to the texture + regular colors and marks the quad dirty; freed re-shows it; permanently failed keeps it; no-placeholder / no-texture / already-loaded cases unchanged; clearing or changing the color while active. 416 tests pass.examples/tests/texture-placeholder-color.tswith four deterministic states (failed-texture placeholder rounded + bordered, loaded image untinted, no-placeholder control). Certifiedchromium-cisnapshot captured in Docker; the full 176-snapshot suite passes in the CI container — relevant because the change touches the quad path for every node.Notes for reviewers
onTextureFailed/onTextureFreedstill setisRenderable = falseimmediately (pre-existing behavior); with a placeholder, the same frame's update pass recomputes it to true before quads are submitted, so there is no blank frame.🤖 Generated with Claude Code