Shader graph: UV, Sample texture, Texture size, Time, Sin/Floor/Fract/Step; edge mode on Output (#78)
game-maker / suite (push) Successful in 8m38s

Fragment-side coordinate nodes instead of vertex nodes: the quad stays the
quad, a UV read plus a sample at a computed coordinate covers flip, scroll,
wave, pixelate and scanlines. Time is a uniform only when a Time node reaches
the Output; a viewport that drew a Time shader redraws from the ticker so the
Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a
second texture variant in TextureLibrary because WebGL binds no TextureStyle
resource of the shader. The compiler now emits #version 300 es first: Pixi
only compiles as ES 3.00 when the fragment says so, and textureSize needs it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-13 16:18:22 +02:00
1 parent 0e5e5ef925
commit f85606427b
12 files changed
+586 -38

No files matched your search

+16 -2
View File
@@ -7,7 +7,7 @@ import type { EditorState } from '../state';
import { type Size, Theme, Widget } from '../../ui';
import type { DrawItem } from '../../viewport/items';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import type { ShaderSource } from '../../viewport/shaders';
import { type ShaderSource, wornShader } from '../../viewport/shaders';
import { disposeSpriteMesh, drawItem, newSpriteMesh, syncSpriteMesh } from '../../viewport/symbols';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { type View, worldBounds, worldToScreen } from '../../viewport/view';
@@ -32,6 +32,7 @@ export abstract class WorldViewport extends Widget {
/** Pictures of sprite items: siblings as well, each right under its node. */
private readonly sprites = new Map<string, Mesh>();
private visibleCount = 0;
private animated = false;
protected lastItems: DrawItem[] = [];
protected constructor(protected readonly state: EditorState) {
@@ -45,8 +46,9 @@ export abstract class WorldViewport extends Widget {
state.on('project', () => this.redraw());
state.on('selection', () => this.redraw());
state.on('mode', () => this.redraw());
// While a Time shader is on screen the ticker redraws every frame anyway (`animate`).
state.on('runtime', () => {
if (this.visible) this.redraw();
if (this.visible && !this.animated) this.redraw();
});
// Unconditional: a picture arriving fires once per image, not per frame.
textureLibrary.onChange(() => this.redraw());
@@ -87,6 +89,16 @@ export abstract class WorldViewport extends Widget {
return { rendering: this.currentView() !== null, visibleItems: this.visibleCount };
}
/** True after a redraw that drew a sprite wearing a Time shader: the picture moves by itself. */
get animates(): boolean {
return this.animated;
}
/** Called every tick: redraws while the last redraw found a Time shader on screen. */
animate(): void {
if (this.visible && this.animated) this.redraw();
}
layerOf(objectId: string): 'objects' | 'overlay' | null {
const node = this.nodes.get(objectId);
if (!node) return null;
@@ -131,6 +143,7 @@ export abstract class WorldViewport extends Widget {
this.contentMask.clear();
this.content.visible = view !== null;
this.visibleCount = 0;
this.animated = false;
if (!view) return;
const { items, background } = this.collectFrame();
@@ -219,6 +232,7 @@ export abstract class WorldViewport extends Widget {
this.objects.addChild(sprite);
}
sprite.visible = true;
if (wornShader(item.props, shaders)?.time) this.animated = true;
syncSpriteMesh(sprite, item, size, textureLibrary, shaders);
}