Let a shader declare a color the sprite sets (#298)
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Successful in 14m11s

A shader could only ask its sprites for numbers. A tint, an outline
colour or a glow colour had to be built from three Property nodes.

- New node Color property next to Property: Name and Default (a
  colour, white unless picked), output `color`. It becomes a colour
  row below the Shader dropdown and a vec3 uniform in the program.
- Number and colour properties share one set of names. A clash is
  the existing "Another property is named" problem.
- Like Color it cannot feed the Vertex output.
- Placed from the palette it is called tint, then tint2.
- The preview in the Shader tab shows a colour field for it. Like
  the sliders it starts at the sprite's value and never writes into
  the project.
- `declaredRows` takes the row builder, so the particle graph keeps
  its number rows unchanged.

Tests:
- Gate: 222 files, 3452 tests (3445 + 7)
- e2e 28: 89 checks (28l-v: default colour drawn, then the colour
  the sprite sets)
- e2e total 1704, counted (1703 + 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 11:43:54 +02:00
1 parent 4f4c149211
commit bf27e42e8a
13 files changed
+303 -57

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
/**
* A shader's `Color property` (#298): a colour row on every sprite wearing it, and a vec3 uniform
* in the program – the colour counterpart of `Property`.
*/
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
import { AddNodeCommand, ConnectCommand } from '../../src/core/commands/graphCommands';
import { findAssetOf } from '../../src/core/model/Project';
import { schemaForAsset } from '../../src/core/properties/PropertySchema';
import {
SHADER_COLOR_PROPERTY,
SHADER_KEY,
SHADER_PROPERTY,
shaderParamDefs,
} from '../../src/core/properties/shaders';
import { compileShader, perCornerProblem } from '../../src/core/shader/compile';
import { SHADER_OUTPUT, SHADER_VERTEX_OUTPUT } from '../../src/core/shader/nodes';
import { paletteEntries } from '../../src/editor/graph/paletteEntries';
import { startValues } from '../../src/editor/shader/previewPicture';
import { makeSpriteScene, roundTrip } from '../helpers';
function shader() {
const f = makeSpriteScene();
const create = new CreateAssetCommand('shader', 'Recolor');
f.stack.execute(create);
const id = create.asset.id;
const asset = () => findAssetOf(f.project, id, 'shader')!;
const add = (type: string, params = {}) => {
const command = new AddNodeCommand(id, type, 0, 0, params);
f.stack.execute(command);
return command.node.id;
};
const link = (from: string, fromPort: string, to: string, toPort: string) =>
f.stack.execute(new ConnectCommand(id, { node: from, port: fromPort }, { node: to, port: toPort }));
const out = add(SHADER_OUTPUT);
return { f, id, asset, add, link, out, build: () => compileShader(asset().graph) };
}
describe('Color property (#298)', () => {
it('declares a colour row, in node order with the number rows', () => {
const s = shader();
s.add(SHADER_PROPERTY, { name: 'amount', default: 0.5, min: 0, max: 1 });
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x3cc85a });
s.add(SHADER_COLOR_PROPERTY, { name: 'shade', default: 'not a colour' });
const rows = shaderParamDefs(s.asset().graph);
expect(rows.map((r) => [r.key, r.kind])).toEqual([
['amount', 'number'],
['tint', 'color'],
['shade', 'color'],
]);
expect(rows[1]).toEqual({ key: 'tint', label: 'tint', kind: 'color', default: 0x3cc85a });
expect(rows[2]!.default).toBe(0xffffff);
});
it('compiles to a vec3 uniform the Output can take as its colour', () => {
const s = shader();
const tint = s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0xff0000 });
const mixed = s.add('ShaderMultiply');
s.link(tint, 'color', mixed, 'a');
s.link(mixed, 'result', s.out, 'color');
const build = s.build();
expect(build.problems).toEqual({});
expect(build.compiled!.uniforms).toEqual([{ name: 'tint', kind: 'color', default: 0xff0000 }]);
expect(build.compiled!.fragment).toContain('uniform vec3 u_tint;');
expect(build.compiled!.fragment).toContain('vec3 n0_color = u_tint;');
expect(build.compiled!.fragment).toContain('vec3 n1_result = (n0_color * vec3(1.0));');
});
it('shares its names with the number properties', () => {
const s = shader();
s.add(SHADER_PROPERTY, { name: 'tint', default: 1 });
const second = s.add(SHADER_COLOR_PROPERTY, { name: 'tint' });
expect(s.build().problems[second]).toBe('Another property is named "tint"');
const rename = s.add(SHADER_COLOR_PROPERTY, { name: 'color' });
expect(s.build().problems[rename]).toBe('"color" is a sprite property');
});
it('is a colour, so it cannot feed the Vertex output', () => {
const s = shader();
const corner = s.add(SHADER_VERTEX_OUTPUT);
const tint = s.add(SHADER_COLOR_PROPERTY, { name: 'tint' });
const bright = s.add('ShaderLuminance');
s.link(tint, 'color', bright, 'color');
s.link(bright, 'value', corner, 'x');
const build = s.build();
expect(build.compiled).toBeNull();
expect(build.problems[corner]).toBe(perCornerProblem('Color property'));
});
it('is placed from the palette as tint, then tint2', () => {
const s = shader();
const entry = () =>
paletteEntries(s.f.project, s.asset()).find((e) => e.def.type === SHADER_COLOR_PROPERTY)!;
expect(entry().params).toEqual({ name: 'tint' });
s.add(SHADER_COLOR_PROPERTY, entry().params);
expect(entry().params).toEqual({ name: 'tint2' });
});
it('gives a sprite wearing it a colour row that survives a save', () => {
const s = shader();
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x112233 });
s.f.stack.execute(new SetAssetPropertyCommand(s.f.spriteId, SHADER_KEY, s.id));
const sprite = () => findAssetOf(s.f.project, s.f.spriteId, 'sprite')!;
expect(schemaForAsset(s.f.project, sprite()).find((d) => d.key === 'tint')).toMatchObject({
kind: 'color',
default: 0x112233,
});
s.f.stack.execute(new SetAssetPropertyCommand(s.f.spriteId, 'tint', 0xe63c3c));
const loaded = findAssetOf(roundTrip(s.f.project), s.f.spriteId, 'sprite')!;
expect(loaded.props.tint).toBe(0xe63c3c);
});
it('starts the preview at the sprite’s colour when it wears the shader, else at the default', () => {
const s = shader();
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x112233 });
const defs = shaderParamDefs(s.asset().graph);
expect(startValues(defs, { [SHADER_KEY]: s.id, tint: '#e63c3c' }, s.id)).toEqual({ tint: 0xe63c3c });
expect(startValues(defs, { [SHADER_KEY]: 'other', tint: 0xe63c3c }, s.id)).toEqual({ tint: 0x112233 });
expect(startValues(defs, { [SHADER_KEY]: s.id }, s.id)).toEqual({ tint: 0x112233 });
});
});
+1 -1
View File
@@ -83,7 +83,7 @@ describe('Vertex output (#79)', () => {
s.link(amount, 'value', s.corner, 'y');
s.link(amount, 'value', s.out, 'alpha');
const { compiled } = s.build();
expect(compiled!.uniforms).toEqual([{ name: 'amount', default: 1 }]);
expect(compiled!.uniforms).toEqual([{ name: 'amount', kind: 'number', default: 1 }]);
expect(compiled!.fragment).toContain('float n0_value = u_amount;');
expect(compiled!.vertex!.body).toContain('float c0_value = u_amount;');
// One precision for a uniform both stages declare: the fragment's default.
+3 -3
View File
@@ -215,9 +215,9 @@ describe('compileShader', () => {
expect(build.reason).toBe('');
const compiled = build.compiled!;
expect(compiled.uniforms).toEqual([
{ name: 'brightness', default: 1 },
{ name: 'contrast', default: 1 },
{ name: 'saturation', default: 1 },
{ name: 'brightness', kind: 'number', default: 1 },
{ name: 'contrast', kind: 'number', default: 1 },
{ name: 'saturation', kind: 'number', default: 1 },
]);
const { fragment } = compiled;
for (const name of ['brightness', 'contrast', 'saturation'])