All files / src/video-filters pal-composite.js

36.84% Statements 7/19
100% Branches 0/0
75% Functions 3/4
36.84% Lines 7/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85                                            6x       128x                                 12x 12x 12x                               5x 5x                                            
// PAL Composite Video Filter - Approach D: Baseband Chroma Blending
//
// Simulates PAL composite video artifacts by encoding the framebuffer to a
// composite signal and decoding it back to RGB, mimicking the behavior of
// a BBC Micro's UHF-modulated picture on a PAL television.
//
// REFERENCES:
// - John Watkinson's "Engineer's Guide to Decoding & Encoding" (Section 3.4)
// - https://www.jim-easterbrook.me.uk/pal/ - Jim Easterbrook's PAL decoder research
// - docs/pal-simulation-design.md - Full implementation details and alternatives tried
// - docs/pal-comb-filter-research.md - Research on authentic PAL TV implementations
 
import VERT_SHADER from "./shaders/pal-composite.vert.glsl?raw";
import FRAG_SHADER from "./shaders/pal-composite.frag.glsl?raw";
import { compileProgram } from "./shader-program.js";
import { PalCyclesPerLine, PalPhasePerLine } from "../video.js";
 
/**
 * How far the raster bows outwards at the middle of each edge, as a fraction of its half
 * size, to roughly match the bezel in tv.png; canvasLeft and canvasTop below place it in
 * the bezel.
 */
export const PalScreenCurvature = { x: 1 / 48, y: 1 / 48 };
 
export class PALCompositeFilter {
    static getDisplayConfig() {
        return {
            name: "PAL TV",
            image: "images/tv.png",
            imageAlt: "A Ferguson television",
            imageWidth: 1000,
            imageHeight: 719,
            canvasLeft: 25,
            canvasTop: 60,
            visibleWidth: 825,
            visibleHeight: 620,
            canvasWidth: 896,
            canvasHeight: 600,
            persistence: { setting: "palPersistenceMs", default: 40 },
        };
    }
 
    constructor(gl) {
        this.gl = gl;
        this.program = compileProgram(gl, VERT_SHADER, FRAG_SHADER, "PAL composite");
        this.locations = {
            uFramebuffer: gl.getUniformLocation(this.program, "uFramebuffer"),
            uResolution: gl.getUniformLocation(this.program, "uResolution"),
            uTexelSize: gl.getUniformLocation(this.program, "uTexelSize"),
            uLineBase: gl.getUniformLocation(this.program, "uLineBase"),
            uPhaseBase: gl.getUniformLocation(this.program, "uPhaseBase"),
            uCyclesPerLine: gl.getUniformLocation(this.program, "uCyclesPerLine"),
            uPhasePerLine: gl.getUniformLocation(this.program, "uPhasePerLine"),
            uExtentCentre: gl.getUniformLocation(this.program, "uExtentCentre"),
            uExtentHalfSize: gl.getUniformLocation(this.program, "uExtentHalfSize"),
            uCurvature: gl.getUniformLocation(this.program, "uCurvature"),
        };
    }
 
    /** Release the GL objects this filter owns. */
    dispose() {
        this.gl.deleteProgram(this.program);
        this.program = null;
    }
 
    setUniforms(params) {
        const gl = this.gl;
        gl.uniform1i(this.locations.uFramebuffer, 0); // Texture unit 0
        gl.uniform2f(this.locations.uResolution, params.width, params.height);
        gl.uniform2f(this.locations.uTexelSize, 1.0 / params.width, 1.0 / params.height);
        gl.uniform2f(this.locations.uLineBase, params.lineBaseEven, params.lineBaseOdd);
        gl.uniform2f(this.locations.uPhaseBase, params.phaseBaseEven, params.phaseBaseOdd);
        gl.uniform1f(this.locations.uCyclesPerLine, PalCyclesPerLine);
        gl.uniform1f(this.locations.uPhasePerLine, PalPhasePerLine);
        const { minx, miny, maxx, maxy } = params.extent;
        gl.uniform2f(this.locations.uExtentCentre, (minx + maxx) / 2 / params.width, (miny + maxy) / 2 / params.height);
        gl.uniform2f(
            this.locations.uExtentHalfSize,
            (maxx - minx) / 2 / params.width,
            (maxy - miny) / 2 / params.height,
        );
        gl.uniform2f(this.locations.uCurvature, PalScreenCurvature.x, PalScreenCurvature.y);
    }
}