Adjust colors: brightness adds instead of multiplying (#62)
game-maker / suite (push) Failing after 4m1s

The shipped shader graph gains a `brightness - 1` offset node, so 1 stays
neutral and brightness lifts every channel by the same amount; contrast keeps
multiplying, saturation keeps mixing with the luma. Fixture, seeds, unit and
e2e expectations follow (twelve nodes, 1.25 -> #80c0ff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-12 22:58:50 +02:00
1 parent 42cafa7543
commit a32b7c6daa
3 files changed
+41 -23

No files matched your search

+22 -6
View File
@@ -46,15 +46,16 @@ function shaderEditor(f: ProjectFixture, name = 'Adjust colors') {
const property = (name: string, value = 1, min = 0, max = 4) => ({ name, default: value, min, max });
/**
* rgb *= brightness; rgb = (rgb - 0.5) * contrast + 0.5; rgb = mix(luma, rgb, saturation) –
* eleven nodes, the same maths the built-in prototype had.
* rgb += brightness - 1; rgb = (rgb - 0.5) * contrast + 0.5; rgb = mix(luma, rgb, saturation) –
* twelve nodes, the recipe `tools/lib/shaders.mjs` ships.
*/
function buildAdjustColors(s: ReturnType<typeof shaderEditor>) {
const texel = s.add(SHADER_TEXTURE_COLOR);
const brightness = s.add(SHADER_PROPERTY, property('brightness'));
const contrast = s.add(SHADER_PROPERTY, property('contrast'));
const saturation = s.add(SHADER_PROPERTY, property('saturation'));
const bright = s.add('ShaderMultiply');
const offset = s.add('ShaderSubtract', { b: 1 });
const bright = s.add('ShaderAdd');
const shifted = s.add('ShaderSubtract', { b: 0.5 });
const scaled = s.add('ShaderMultiply');
const centred = s.add('ShaderAdd', { b: 0.5 });
@@ -62,7 +63,8 @@ function buildAdjustColors(s: ReturnType<typeof shaderEditor>) {
const mixed = s.add('ShaderMix');
const output = s.add(SHADER_OUTPUT);
s.link(texel, 'color', bright, 'a');
s.link(brightness, 'value', bright, 'b');
s.link(brightness, 'value', offset, 'a');
s.link(offset, 'result', bright, 'b');
s.link(bright, 'result', shifted, 'a');
s.link(shifted, 'result', scaled, 'a');
s.link(contrast, 'value', scaled, 'b');
@@ -72,7 +74,20 @@ function buildAdjustColors(s: ReturnType<typeof shaderEditor>) {
s.link(centred, 'result', mixed, 'b');
s.link(saturation, 'value', mixed, 't');
s.link(mixed, 'result', output, 'color');
return { texel, brightness, contrast, saturation, bright, shifted, scaled, centred, luma, mixed, output };
return {
texel,
brightness,
contrast,
saturation,
offset,
bright,
shifted,
scaled,
centred,
luma,
mixed,
output,
};
}
describe('a shader asset', () => {
@@ -161,7 +176,8 @@ describe('compileShader', () => {
for (const name of ['brightness', 'contrast', 'saturation'])
expect(fragment).toContain(`uniform float u_${name};`);
expect(fragment).toContain('vec3 rgb = texel.a > 0.0 ? texel.rgb / texel.a : texel.rgb;');
expect(fragment).toMatch(/vec3 n\d+_result = \(n\d+_color \* vec3\(n\d+_value\)\);/);
expect(fragment).toMatch(/float n\d+_result = \(n\d+_value - 1\.0\);/);
expect(fragment).toMatch(/vec3 n\d+_result = \(n\d+_color \+ vec3\(n\d+_result\)\);/);
expect(fragment).toMatch(/vec3 n\d+_result = \(n\d+_result - vec3\(0\.5\)\);/);
expect(fragment).toMatch(/float n\d+_value = dot\(n\d+_result, vec3\(0\.2126, 0\.7152, 0\.0722\)\);/);
expect(fragment).toMatch(/vec3 n\d+_result = mix\(vec3\(n\d+_value\), n\d+_result, n\d+_value\);/);
+7 -7
View File
@@ -53,8 +53,8 @@ export async function shader(t) {
// 28a. The fixture ships Adjust colors as an asset; the + menu makes another and opens its tab on it.
const seeded = (await shaders()).find((s) => s.id === ADJUST);
check(
'the fixture carries the Adjust colors shader as an asset of eleven nodes',
Boolean(seeded) && seeded.nodes.length === 11,
'the fixture carries the Adjust colors shader as an asset of twelve nodes',
Boolean(seeded) && seeded.nodes.length === 12,
JSON.stringify(seeded),
);
await clickText('+');
@@ -226,10 +226,10 @@ export async function shader(t) {
colourDistance(neutral, base) < 6,
JSON.stringify({ base, neutral }),
);
await assetProps({ brightness: 0.5 });
await assetProps({ brightness: 1.25 });
await sleep(300);
const darker = await centre();
check('brightness 0.5 halves every channel', colourDistance(darker, '#204080') < 24, darker);
const lighter = await centre();
check('brightness 1.25 adds a quarter to every channel', colourDistance(lighter, '#80c0ff') < 24, lighter);
await assetProps({ brightness: 1, contrast: 0 });
await sleep(300);
const flat = await centre();
@@ -241,14 +241,14 @@ export async function shader(t) {
await shot('28-shader-desaturated');
// 28f. The game draws through the same path, so play shows the shader too.
await assetProps({ saturation: 1, brightness: 0.5 });
await assetProps({ saturation: 1, brightness: 1.25 });
await play();
await sleep(500);
const at = await json("__gm.gameScreen('oshade')");
const played = at ? await json(`__gm.pixelAt(${Math.round(at.x + 20)}, ${Math.round(at.y + 20)})`) : null;
check(
'during play the sprite is drawn with its shader',
played !== null && colourDistance(played, '#204080') < 24,
played !== null && colourDistance(played, '#80c0ff') < 24,
JSON.stringify({ at, played }),
);
await stop();
+12 -10
View File
@@ -2,8 +2,8 @@ import { edge, node } from './builders.mjs';
/**
* The "Adjust colors" shader as an asset document: brightness, contrast and saturation, 1 neutral,
* on the straight colour – rgb *= brightness; rgb = (rgb - 0.5) * contrast + 0.5;
* rgb = mix(luma, rgb, saturation). The recipe the built-in prototype of #62 had, now eleven nodes.
* on the straight colour – rgb += brightness - 1; rgb = (rgb - 0.5) * contrast + 0.5;
* rgb = mix(luma, rgb, saturation). Brightness adds (user decision, #62 follow-up), so 1 is +0.
*/
export function adjustColorsShader(id = 'adjust-colors', name = 'Adjust colors') {
const property = (nodeId, propertyName, x, y) =>
@@ -19,17 +19,19 @@ export function adjustColorsShader(id = 'adjust-colors', name = 'Adjust colors')
property(`${id}-brightness`, 'brightness', 0, 160),
property(`${id}-contrast`, 'contrast', 0, 320),
property(`${id}-saturation`, 'saturation', 0, 480),
node(`${id}-bright`, 'ShaderMultiply', 300, 0),
node(`${id}-shift`, 'ShaderSubtract', 560, 0, { b: 0.5 }),
node(`${id}-scale`, 'ShaderMultiply', 820, 0),
node(`${id}-centre`, 'ShaderAdd', 1080, 0, { b: 0.5 }),
node(`${id}-luma`, 'ShaderLuminance', 1340, 160),
node(`${id}-mix`, 'ShaderMix', 1600, 0),
node(`${id}-out`, 'ShaderOutput', 1860, 0),
node(`${id}-offset`, 'ShaderSubtract', 260, 160, { b: 1 }),
node(`${id}-bright`, 'ShaderAdd', 520, 0),
node(`${id}-shift`, 'ShaderSubtract', 780, 0, { b: 0.5 }),
node(`${id}-scale`, 'ShaderMultiply', 1040, 0),
node(`${id}-centre`, 'ShaderAdd', 1300, 0, { b: 0.5 }),
node(`${id}-luma`, 'ShaderLuminance', 1560, 160),
node(`${id}-mix`, 'ShaderMix', 1820, 0),
node(`${id}-out`, 'ShaderOutput', 2080, 0),
],
edges: [
edge(`${id}-e1`, `${id}-texel`, 'color', `${id}-bright`, 'a'),
edge(`${id}-e2`, `${id}-brightness`, 'value', `${id}-bright`, 'b'),
edge(`${id}-e2`, `${id}-brightness`, 'value', `${id}-offset`, 'a'),
edge(`${id}-e12`, `${id}-offset`, 'result', `${id}-bright`, 'b'),
edge(`${id}-e3`, `${id}-bright`, 'result', `${id}-shift`, 'a'),
edge(`${id}-e4`, `${id}-shift`, 'result', `${id}-scale`, 'a'),
edge(`${id}-e5`, `${id}-contrast`, 'value', `${id}-scale`, 'b'),