/** * A camera wears a shader over its whole picture (#299): the rows, and the picture it is laid over – * the camera's raster or the screen's pixels, with a pixel counted as a camera pixel in both. */ import { describe, expect, it } from 'vitest'; import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands'; import { AddNodeCommand } from '../../src/core/commands/graphCommands'; import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'; import { animatableDefsOf } from '../../src/core/model/animation'; import { findAssetOf } from '../../src/core/model/Project'; import { isRowShown, schemaForAsset, schemaForObject } from '../../src/core/properties/PropertySchema'; import { SHADER_RESOLUTION_KEY } from '../../src/core/properties/cameraShader'; import { SHADER_KEY, SHADER_PROPERTY } from '../../src/core/properties/shaders'; import { CAMERA_MAX_PIXELS, cameraPicture } from '../../src/viewport/cameraPicture'; import { makeSpriteScene, place, roundTrip } from '../helpers'; function wearing(uniform = 'fade') { const f = makeSpriteScene(); const camera = new CreateAssetCommand('camera', 'Cam'); const shader = new CreateAssetCommand('shader', 'Fade'); f.stack.execute(camera); f.stack.execute(shader); const declared = { name: uniform, default: 0.5, min: 0, max: 1 }; f.stack.execute(new AddNodeCommand(shader.asset.id, SHADER_PROPERTY, 0, 0, declared)); const cameraId = camera.asset.id; const shaderId = shader.asset.id; const asset = () => findAssetOf(f.project, cameraId, 'camera')!; const rows = () => schemaForAsset(f.project, asset()); const wear = () => f.stack.execute(new SetAssetPropertyCommand(cameraId, SHADER_KEY, shaderId)); return { ...f, cameraId, shaderId, asset, rows, wear }; } /** A pixel of the picture back in world units: the inverse of `toPicture` along each axis. */ function worldAt(p: ReturnType, px: number, py: number) { const t = p.toPicture; return { x: (px - t.tx) / t.a, y: (py - t.ty) / t.d }; } describe('a camera wearing a shader', () => { it('has the picker and the resolution, the resolution shown only with a shader picked', () => { const f = wearing(); const resolution = f.rows().find((d) => d.key === SHADER_RESOLUTION_KEY)!; expect(resolution).toMatchObject({ kind: 'enum', default: 'screen' }); expect(isRowShown(resolution, f.asset().props)).toBe(false); expect(f.rows().map((d) => d.key)).not.toContain('fade'); f.wear(); expect(isRowShown(resolution, f.asset().props)).toBe(true); expect(f.rows().at(-1)).toMatchObject({ key: 'fade', kind: 'number', default: 0.5 }); }); it('keeps a name it already has: `zoom` stays the camera’s own row', () => { const f = wearing('zoom'); f.wear(); expect(f.rows().filter((d) => d.key === 'zoom')).toEqual([expect.objectContaining({ label: 'Zoom' })]); }); it('gives a placed camera the rows of the shader it overrides, and keeps them through a save', () => { const f = wearing(); const id = place(f, f.cameraId, { x: 0, y: 0 }); f.stack.execute(new SetOverrideCommand(f.sceneId, id, SHADER_KEY, f.shaderId)); f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'fade', 0.25)); expect(schemaForObject(f.project, f.asset(), f.object(id)).map((d) => d.key)).toContain('fade'); const placed = findAssetOf(roundTrip(f.project), f.sceneId, 'scene')!.objects.find((o) => o.id === id)!; expect(placed.overrides).toEqual({ shader: f.shaderId, fade: 0.25 }); }); it('offers the shader’s parameters to an animation', () => { const f = wearing(); f.wear(); expect(animatableDefsOf(f.project, f.asset()).map((d) => d.key)).toContain('fade'); }); }); describe('the picture a camera’s shader is laid over', () => { const camera = (props: Record = {}) => ({ x: 100, y: -50, props: { width: 640, height: 360, zoom: 1, shader: 'fx', ...props }, }); it('has as many pixels as the frame covers on screen, and counts each as a camera pixel', () => { const p = cameraPicture(camera(), 2); expect(p.pixels).toEqual({ w: 1280, h: 720 }); expect(p.pixelScale).toEqual({ x: 0.5, y: 0.5 }); expect(p.blocky).toBe(false); expect(worldAt(p, 0, 0)).toEqual({ x: -220, y: -230 }); expect(worldAt(p, 1280, 720)).toEqual({ x: 420, y: 130 }); }); it('has the camera’s raster with Camera pixels, at any zoom on screen', () => { for (const ppu of [0.5, 1, 3]) { const p = cameraPicture(camera({ [SHADER_RESOLUTION_KEY]: 'camera', zoom: 2 }), ppu); expect(p.pixels).toEqual({ w: 640, h: 360 }); expect(p.pixelScale).toEqual({ x: 1, y: 1 }); expect(p.blocky).toBe(true); expect(p.frame).toEqual({ w: 320, h: 180 }); expect(worldAt(p, 640, 360)).toEqual({ x: 260, y: 40 }); } }); it('caps the picture and still counts camera pixels', () => { const p = cameraPicture(camera(), 10); expect(p.pixels).toEqual({ w: CAMERA_MAX_PIXELS, h: 3600 }); expect(p.pixelScale.x).toBeCloseTo(640 / CAMERA_MAX_PIXELS); }); });