Documentation/Source example
Example source

samples/vis/Builtin - Interactive Showcase.js

Referenced by:vis.Create
include('docs/Flags.js');

// JSplitter vis: interactive built-in visualization showcase.
// Click the controls to change the analysis cadence, paint rate and GDI/D2D backend.
// Click any visualization tile to cycle a compact preset of its most visible
// source and renderer properties. Playback is never started or changed here.

const ENGINE_INTERVALS = [8, 16, 32];
const PAINT_RATES = [30, 60, 120];
const DRAW_MODES = [
    {value:0, text:'GDI'},
    {value:1, text:'D2D'}
];
const INITIAL_ENGINE_INTERVAL = vis.UpdateIntervalMs;

const COLOR = {
    background: 0xff101317,
    header:     0xff151a20,
    tile:       0xff12171c,
    tileHover:  0xff1a222b,
    border:     0xff2a333d,
    text:       0xffe7edf3,
    muted:      0xff91a0ae,
    accent:     0xff5aa9ff,
    button:     0xff232b34,
    buttonHover:0xff303b47,
    white:      0xffedf2f7,
    blue:       0xff63a4ff,
    cyan:       0xff55d6d9,
    green:      0xff63d471,
    yellow:     0xffffd166,
    orange:     0xffff9f43,
    red:        0xffff5d73,
    purple:     0xffc792ea,
    black:      0xff000000
};

let TITLE_FONT = null;
let CONTROL_FONT = null;
let LABEL_FONT = null;
let META_FONT = null;

const HEADER_H = 64;
const GRID_MARGIN = 8;
const GRID_GAP = 7;
const TILE_LABEL_H = 22;

const SPEC_RANGES_A = [
    {to:-36, color:COLOR.blue},
    {to:-18, color:COLOR.green},
    {to:-6, color:COLOR.yellow},
    {to:0, color:COLOR.red}
];
const SPEC_RANGES_B = [
    {to:-48, color:COLOR.purple},
    {to:-24, color:COLOR.cyan},
    {to:-12, color:COLOR.green},
    {to:0, color:COLOR.orange}
];
const METER_RANGES_A = [
    {to:-18, color:COLOR.green},
    {to:-6, color:COLOR.yellow},
    {to:0, color:COLOR.red}
];
const METER_RANGES_B = [
    {to:-24, color:COLOR.blue},
    {to:-9, color:COLOR.green},
    {to:-3, color:COLOR.yellow},
    {to:0, color:COLOR.red}
];
const VU_RANGES_A = [
    {to:-10, color:COLOR.green},
    {to:0, color:COLOR.yellow},
    {to:3, color:COLOR.red}
];
const VU_RANGES_B = [
    {to:-12, color:COLOR.cyan},
    {to:-3, color:COLOR.green},
    {to:0, color:COLOR.yellow},
    {to:6, color:COLOR.red}
];

function preset(name, values) {
    return {name, values};
}

function spectrum(overrides) {
    return Object.assign({
        Channel:'mix', FftSize:4096, Bands:96, BarWidth:0, Gap:1,
        SegmentSize:6, SegmentGap:0, ColorMode:'gradient',
        MinFrequency:40, MaxFrequency:20000, MinDb:-78, MaxDb:0,
        AttackMs:18, ReleaseMs:120, PeakHoldMs:180, PeakGravity:3,
        Gain:1, Ranges:SPEC_RANGES_A, ShowPeak:true,
        PeakColor:COLOR.white, PeakSize:1
    }, overrides || {});
}

function waveform(overrides) {
    return Object.assign({
        Channel:'mix', Points:'auto', DurationMs:300, Gain:1,
        LineWidth:1.25, GlowStrength:0.2, GlowRadius:3, Smoothing:0.1,
        Style:'line', Color:COLOR.cyan
    }, overrides || {});
}

function peak(overrides) {
    return Object.assign({
        Channel:'mix', MinDb:-60, MaxDb:0, AttackMs:0, ReleaseMs:150,
        PeakHoldMs:500, PeakGravity:3, Ranges:METER_RANGES_A,
        SegmentSize:6, SegmentGap:0, ColorMode:'gradient',
        PeakColor:COLOR.white, PeakSize:1, ShowPeak:true,
        ClipWidth:4, ShowClip:false, ClipHoldMs:1000, ClipColor:COLOR.red
    }, overrides || {});
}

function vu(overrides) {
    return Object.assign({
        Channel:'mix', ReferenceDbfs:-18, MinVu:-20, MaxVu:3,
        AttackMs:300, ReleaseMs:300, PeakHoldMs:150, PeakGravity:3,
        Ranges:VU_RANGES_A, SegmentSize:6, SegmentGap:0,
        ColorMode:'gradient', PeakColor:COLOR.white,
        PeakSize:1, ShowPeak:false
    }, overrides || {});
}

function spectrogram(overrides) {
    return Object.assign({
        Channel:'mix', Style:'scrolling', FftSize:4096,
        ColumnIntervalMs:16, MinFrequency:40, MaxFrequency:20000,
        MinDb:-84, MaxDb:0, Gain:1, Scale:'log', Palette:'inferno',
        Color:COLOR.white, BackgroundColor:COLOR.black
    }, overrides || {});
}

function vectorscope(overrides) {
    return Object.assign({
        DurationMs:40, Points:1024, LeftChannel:0, RightChannel:1,
        Color:COLOR.cyan, Mode:'points', Scale:1, AspectScale:true,
        PointSize:1.5, LineWidth:1, GlowStrength:0.25, GlowRadius:3,
        PersistenceMs:300
    }, overrides || {});
}

const SPECTRUM_PRESETS = [
    preset('Smooth', spectrum()),
    preset('Detail', spectrum({
        // Keep the dense preset away from a 1 px bar/gap checkerboard. Two
        // adjacent tiles with that pattern produce display-scaling moire.
        FftSize:16384, Bands:192, Gap:0, SegmentSize:5, SegmentGap:2,
        ColorMode:'discrete', MinFrequency:25, MaxFrequency:22000,
        MinDb:-96, ReleaseMs:170, PeakHoldMs:320, PeakGravity:2,
        Ranges:SPEC_RANGES_B, PeakSize:2
    })),
    preset('Bars', spectrum({
        FftSize:2048, Bands:80, BarWidth:5, Gap:2,
        SegmentSize:7, SegmentGap:3, ColorMode:'discrete',
        MinDb:-72, AttackMs:8, ReleaseMs:80, Gain:1.15,
        Ranges:SPEC_RANGES_A, ShowPeak:false
    })),
    preset('Gradient', spectrum({
        FftSize:4096, Bands:112, BarWidth:0, Gap:1,
        SegmentSize:5, SegmentGap:2, ColorMode:'gradient',
        MinFrequency:35, MaxFrequency:20000, MinDb:-84,
        AttackMs:12, ReleaseMs:130, PeakHoldMs:220, PeakGravity:3,
        Ranges:SPEC_RANGES_B, PeakSize:2
    }))
];

const WAVEFORM_PRESETS = [
    preset('Trace', waveform()),
    preset('Envelope', waveform({
        Points:512, DurationMs:500, Gain:1.25, LineWidth:2,
        GlowStrength:0.5, GlowRadius:7, Smoothing:0.4,
        Style:'envelope', Color:COLOR.purple
    })),
    preset('Fast', waveform({
        Points:192, DurationMs:80, Gain:1.4, LineWidth:1.5,
        GlowStrength:0.15, GlowRadius:2, Smoothing:0.05,
        Style:'line', Color:COLOR.green
    })),
    preset('Glow', waveform({
        Points:384, DurationMs:220, Gain:1.35, LineWidth:1,
        GlowStrength:0.5, GlowRadius:4, Smoothing:0.25,
        Style:'line', Color:COLOR.blue
    }))
];

const PEAK_PRESETS = [
    preset('Smooth', peak()),
    preset('Gradient', peak({
        MinDb:-66, ReleaseMs:180, PeakHoldMs:600, PeakGravity:2.5,
        Ranges:METER_RANGES_B, SegmentSize:6, SegmentGap:2,
        ColorMode:'gradient', PeakSize:2, ShowClip:true,
        ClipWidth:4, ClipHoldMs:1200
    })),
    preset('Discrete', peak({
        MinDb:-72, ReleaseMs:220, PeakHoldMs:750, PeakGravity:2,
        Ranges:METER_RANGES_B, SegmentSize:5, SegmentGap:2,
        ColorMode:'discrete', PeakSize:3, ShowClip:true,
        ClipWidth:5, ClipHoldMs:1500
    })),
    preset('Quick', peak({
        MinDb:-48, ReleaseMs:70, PeakHoldMs:100, PeakGravity:6,
        SegmentSize:3, SegmentGap:1, ColorMode:'discrete',
        PeakSize:1, ShowClip:true, ClipWidth:3, ClipHoldMs:400
    }))
];

const VU_PRESETS = [
    preset('Classic', vu()),
    preset('Gradient', vu({
        ReferenceDbfs:-18, MinVu:-24, MaxVu:3,
        AttackMs:260, ReleaseMs:320, PeakHoldMs:250, PeakGravity:3,
        Ranges:VU_RANGES_B, SegmentSize:6, SegmentGap:2,
        ColorMode:'gradient', PeakSize:2, ShowPeak:true
    })),
    preset('Wide', vu({
        ReferenceDbfs:-20, MinVu:-40, MaxVu:6,
        AttackMs:500, ReleaseMs:550, PeakHoldMs:500, PeakGravity:1.5,
        Ranges:VU_RANGES_B,
        SegmentSize:8, SegmentGap:0, ColorMode:'gradient',
        PeakSize:3, ShowPeak:true
    })),
    preset('Discrete', vu({
        ReferenceDbfs:-18, MinVu:-24, MaxVu:3,
        AttackMs:180, ReleaseMs:220, PeakHoldMs:100, PeakGravity:5,
        SegmentSize:5, SegmentGap:2, ColorMode:'discrete',
        PeakSize:2, ShowPeak:true
    }))
];

const SPECTROGRAM_PRESETS = [
    preset('Scroll', spectrogram()),
    preset('Stationary', spectrogram({
        Style:'stationary', FftSize:8192, ColumnIntervalMs:32,
        MinFrequency:30, MaxFrequency:18000, MinDb:-96,
        Gain:1.15, Scale:'linear', Palette:'grayscale', Color:COLOR.cyan
    })),
    preset('Fast', spectrogram({
        Style:'scrolling', FftSize:2048, ColumnIntervalMs:8,
        MinFrequency:60, MaxFrequency:16000, MinDb:-72,
        Gain:1.2, Scale:'log', Palette:'inferno'
    }))
];

const VECTORSCOPE_PRESETS = [
    preset('Points', vectorscope()),
    preset('Lines', vectorscope({
        DurationMs:80, Points:2048, Mode:'lines', Scale:1.15, LineWidth:1.5,
        GlowStrength:0.55, GlowRadius:7, PersistenceMs:700,
        Color:COLOR.purple
    })),
    preset('Wide', vectorscope({
        DurationMs:160, Points:4096, Mode:'points', Scale:1.4,
        AspectScale:false, PointSize:2.25, GlowStrength:0.5,
        GlowRadius:7, PersistenceMs:1100, Color:COLOR.orange
    }))
];

const tiles = [
    {name:'Spectrum A', type:'spectrum', source:VisSourceType.Spectrum, presets:SPECTRUM_PRESETS, index:0},
    {name:'Spectrum B', type:'spectrum', source:VisSourceType.Spectrum, presets:SPECTRUM_PRESETS, index:1},
    {name:'Waveform A', type:'waveform', source:VisSourceType.Waveform, presets:WAVEFORM_PRESETS, index:0},
    {name:'Waveform B', type:'waveform', source:VisSourceType.Waveform, presets:WAVEFORM_PRESETS, index:1},
    {name:'Peak Meter', type:'peak', source:VisSourceType.PeakMeter, presets:PEAK_PRESETS, index:0},
    {name:'VU Meter', type:'vu', source:VisSourceType.VUMeter, presets:VU_PRESETS, index:0},
    {name:'Spectrogram A', type:'spectrogram', source:VisSourceType.Spectrogram, presets:SPECTROGRAM_PRESETS, index:0},
    {name:'Spectrogram B', type:'spectrogram', source:VisSourceType.Spectrogram, presets:SPECTROGRAM_PRESETS, index:1},
    {name:'Vectorscope', type:'vectorscope', source:VisSourceType.Vectorscope, presets:VECTORSCOPE_PRESETS, index:0}
];

let engineInterval = 16;
let paintRate = 30;
let drawMode = window.DrawMode === 1 ? 1 : 0;
let paintTimer = null;
let switchingBackend = false;
let hoveredControl = -1;
let hoveredTile = -1;
let controlRects = [];
let tileRects = [];

function releaseDrawingResources() {
    TITLE_FONT = null;
    CONTROL_FONT = null;
    LABEL_FONT = null;
    META_FONT = null;

    // Visualization instances own backend-specific bitmap/history caches.
    // Drop them together with script drawing resources before DrawMode changes.
    for (const tile of tiles) {
        tile.visual = null;
        tile.visuals = null;
    }
}

function createDrawingResources() {
    // gdi.Font() follows window.DrawMode and creates GdiFont or D2DFont.
    TITLE_FONT = gdi.Font('Segoe UI', 13);
    CONTROL_FONT = gdi.Font('Segoe UI', 10);
    LABEL_FONT = gdi.Font('Segoe UI', 11);
    META_FONT = gdi.Font('Segoe UI', 9);

    // This sample has no script-owned bitmaps. Any added later must be created
    // here as well because gdi.Image/CreateImage also follow window.DrawMode.
}

function currentPreset(tile) {
    return tile.presets[tile.index];
}

function applyProperties(object, values) {
    for (const key of Object.keys(values)) object[key] = values[key];
}

function isStereoMeter(tile) {
    return tile.type === 'peak' || tile.type === 'vu';
}

function createOrUpdateTile(tile, recreate) {
    const selected = currentPreset(tile);
    if (isStereoMeter(tile)) {
        if (!tile.visuals || recreate) {
            tile.visuals = [
                vis.Create(tile.source, {}, {}),
                vis.Create(tile.source, {}, {})
            ];
        }
        for (let channel = 0; channel < tile.visuals.length; ++channel) {
            applyProperties(tile.visuals[channel], selected.values);
            tile.visuals[channel].Channel = channel;
        }
        return;
    }

    if (!tile.visual || recreate) tile.visual = vis.Create(tile.source, {}, {});
    applyProperties(tile.visual, selected.values);
}

function cycleTile(index) {
    const tile = tiles[index];
    tile.index = (tile.index + 1) % tile.presets.length;
    createOrUpdateTile(tile, false);
}

function setEngineInterval(value) {
    engineInterval = value;
    vis.UpdateIntervalMs = value;
}

function setPaintRate(value) {
    paintRate = value;
    if (paintTimer) clearInterval(paintTimer);
    paintTimer = setInterval(() => window.Repaint(), 1000 / value);
}

function setDrawMode(value) {
    value = value === 1 ? 1 : 0;
    if (value === drawMode) return;

    // DrawMode performs a synchronous repaint. Keep on_paint away from resources
    // that belong to the old backend until replacements have been created.
    switchingBackend = true;
    try {
        releaseDrawingResources();
        window.DrawMode = value;
        drawMode = window.DrawMode === 1 ? 1 : 0;
        createDrawingResources();
        for (const tile of tiles) createOrUpdateTile(tile, true);
    } finally {
        switchingBackend = false;
    }
    window.Repaint();
}

function addControl(group, value, text, x, y, w, h) {
    controlRects.push({group, value, text, x, y, w, h});
}

function updateLayout() {
    controlRects = [];
    const buttonY = 33;
    const buttonH = 23;
    const buttonW = 42;
    const buttonGap = 4;

    let x = 82;
    for (const value of ENGINE_INTERVALS) {
        addControl('engine', value, String(value), x, buttonY, buttonW, buttonH);
        x += buttonW + buttonGap;
    }
    x += 84;
    for (const value of PAINT_RATES) {
        addControl('paint', value, String(value), x, buttonY, buttonW, buttonH);
        x += buttonW + buttonGap;
    }
    x += 70;
    for (const mode of DRAW_MODES) {
        addControl('backend', mode.value, mode.text, x, buttonY, 52, buttonH);
        x += 52 + buttonGap;
    }

    tileRects = [];
    const availableW = Math.max(0, window.Width - GRID_MARGIN * 2 - GRID_GAP * 2);
    const availableH = Math.max(0, window.Height - HEADER_H - GRID_MARGIN - GRID_GAP * 2);
    const cellW = availableW / 3;
    const cellH = availableH / 3;
    for (let i = 0; i < tiles.length; ++i) {
        const col = i % 3;
        const row = Math.floor(i / 3);
        tileRects.push({
            x:GRID_MARGIN + col * (cellW + GRID_GAP),
            y:HEADER_H + row * (cellH + GRID_GAP),
            w:cellW,
            h:cellH
        });
    }
}

function contains(rect, x, y) {
    return x >= rect.x && y >= rect.y && x < rect.x + rect.w && y < rect.y + rect.h;
}

function hitControl(x, y) {
    for (let i = 0; i < controlRects.length; ++i) {
        if (contains(controlRects[i], x, y)) return i;
    }
    return -1;
}

function hitTile(x, y) {
    for (let i = 0; i < tileRects.length; ++i) {
        if (contains(tileRects[i], x, y)) return i;
    }
    return -1;
}

function formatChannel(value) {
    return value === 'mix' ? 'mix' : 'ch ' + (Number(value) + 1);
}

function describeTile(tile) {
    const selected = currentPreset(tile);
    if (tile.type === 'vectorscope') {
        return selected.name + ' · ' + selected.values.Points + ' pts · ' +
            selected.values.DurationMs + ' ms · ' + selected.values.Mode +
            ' · trail ' + selected.values.PersistenceMs + ' ms';
    }

    const value = selected.values;
    switch (tile.type) {
        case 'spectrum':
            return selected.name + ' · FFT ' + value.FftSize + ' · ' + value.Bands +
                ' bands · ' + value.ColorMode;
        case 'waveform': {
            let points = value.Points;
            if (points === 'auto' && tile.visual) points = 'auto/' + tile.visual.ActualPoints;
            return selected.name + ' · ' + points + ' pts · ' + value.DurationMs +
                ' ms · ' + value.Style + ' · smooth ' + value.Smoothing;
        }
        case 'peak':
            return selected.name + ' · stereo · ' + value.MinDb + '..' + value.MaxDb +
                ' dB · release ' + value.ReleaseMs + ' ms · ' + value.ColorMode;
        case 'vu':
            return selected.name + ' · stereo · 0 VU=' + value.ReferenceDbfs + ' dBFS · ' +
                value.MinVu + '..+' + value.MaxVu + ' · ' + value.ColorMode;
        case 'spectrogram':
            return selected.name + ' · FFT ' + value.FftSize + ' · ' +
                value.ColumnIntervalMs + ' ms · ' + value.Scale + ' · ' + value.Palette;
    }
    return selected.name + ' · ' + formatChannel(value.Channel);
}

function drawButton(gr, rect, index) {
    const active = rect.group === 'engine' ? rect.value === engineInterval
        : rect.group === 'paint' ? rect.value === paintRate
        : rect.value === drawMode;
    const hovered = index === hoveredControl;
    const fill = active ? COLOR.accent : hovered ? COLOR.buttonHover : COLOR.button;
    const textColor = active ? COLOR.black : COLOR.text;
    gr.FillSolidRect(rect.x, rect.y, rect.w, rect.h, fill);
    gr.DrawRect(rect.x + 0.5, rect.y + 0.5, rect.w - 1, rect.h - 1,
        1, active ? COLOR.accent : COLOR.border);
    const textW = gr.CalcTextWidth(rect.text, CONTROL_FONT);
    gr.DrawString(rect.text, CONTROL_FONT, textColor,
        rect.x + (rect.w - textW) * 0.5, rect.y + 3, textW + 2, rect.h - 4);
}

function drawHeader(gr) {
    gr.FillSolidRect(0, 0, window.Width, HEADER_H, COLOR.header);
    gr.FillSolidRect(0, HEADER_H - 1, window.Width, 1, COLOR.border);
    gr.DrawString('Visualization demo', TITLE_FONT, COLOR.text,
        10, 6, Math.max(0, window.Width - 20), 22);
    gr.DrawString('Engine ms', CONTROL_FONT, COLOR.muted, 10, 37, 70, 18);
    gr.DrawString('Paint fps', CONTROL_FONT, COLOR.muted, 236, 37, 70, 18);
    gr.DrawString('Backend', CONTROL_FONT, COLOR.muted, 448, 37, 60, 18);
    gr.SetSmoothingMode(SmoothingMode.HighQuality);
    try {
        for (let i = 0; i < controlRects.length; ++i) drawButton(gr, controlRects[i], i);
    }
    finally {
        gr.SetSmoothingMode(SmoothingMode.Default);
    }

    const hintX = 640;
    if (window.Width > hintX + 120) {
        gr.DrawString('Click any tile to switch its preset', CONTROL_FONT, COLOR.muted,
            hintX, 38, window.Width - hintX - 10, 18);
    }
}

function drawTile(gr, tile, rect, index) {
    if (rect.w <= 0 || rect.h <= 0) return;
    const hovered = index === hoveredTile;
    gr.FillSolidRect(rect.x, rect.y, rect.w, rect.h,
        hovered ? COLOR.tileHover : COLOR.tile);

    const drawX = rect.x + 4;
    const drawY = rect.y + TILE_LABEL_H;
    const drawW = Math.max(0, rect.w - 8);
    const drawH = Math.max(0, rect.h - TILE_LABEL_H - 4);
    if (isStereoMeter(tile) && tile.visuals && drawW > 0 && drawH > 0) {
        const channelGap = 4;
        const firstH = Math.max(0, Math.floor((drawH - channelGap) * 0.5));
        const secondH = Math.max(0, drawH - channelGap - firstH);
        try {
            tile.visuals[0].Draw(gr, drawX, drawY, drawW, firstH);
            tile.visuals[1].Draw(gr, drawX, drawY + firstH + channelGap, drawW, secondH);
        } catch (e) {
            console.log('Visualization showcase: ' + tile.name + ': ' + e);
        }
    } else if (tile.visual && drawW > 0 && drawH > 0) {
        try {
            tile.visual.Draw(gr, drawX, drawY, drawW, drawH);
        } catch (e) {
            console.log('Visualization showcase: ' + tile.name + ': ' + e);
        }
    }

    gr.FillSolidRect(rect.x, rect.y, rect.w, TILE_LABEL_H,
        hovered ? COLOR.tileHover : COLOR.tile);
    const labelX = rect.x + 6;
    const labelY = rect.y + 2;
    gr.DrawString(tile.name, LABEL_FONT, COLOR.text,
        labelX, labelY, Math.max(0, rect.w - 12), TILE_LABEL_H - 2);
    const nameW = gr.CalcTextWidth(tile.name, LABEL_FONT);
    const metaX = labelX + nameW + 5;
    gr.DrawString('| ' + describeTile(tile), META_FONT, COLOR.muted,
        metaX, labelY + 2, Math.max(0, rect.x + rect.w - metaX - 5), TILE_LABEL_H - 4);

    gr.DrawRect(rect.x + 0.5, rect.y + 0.5, Math.max(0, rect.w - 1),
        Math.max(0, rect.h - 1), hovered ? 1.5 : 1,
        hovered ? COLOR.accent : COLOR.border);
}

function on_paint(gr) {
    gr.FillSolidRect(0, 0, window.Width, window.Height, COLOR.background);
    if (switchingBackend) return;
    updateLayout();
    drawHeader(gr);
    for (let i = 0; i < tiles.length; ++i) drawTile(gr, tiles[i], tileRects[i], i);
}

function on_mouse_move(x, y) {
    updateLayout();
    const nextControl = hitControl(x, y);
    const nextTile = nextControl === -1 ? hitTile(x, y) : -1;
    if (nextControl !== hoveredControl || nextTile !== hoveredTile) {
        hoveredControl = nextControl;
        hoveredTile = nextTile;
        window.Repaint();
    }
}

function on_mouse_leave() {
    if (hoveredControl !== -1 || hoveredTile !== -1) {
        hoveredControl = -1;
        hoveredTile = -1;
        window.Repaint();
    }
}

function on_mouse_lbtn_up(x, y) {
    updateLayout();
    const control = hitControl(x, y);
    if (control !== -1) {
        const selected = controlRects[control];
        if (selected.group === 'engine') setEngineInterval(selected.value);
        else if (selected.group === 'paint') setPaintRate(selected.value);
        else setDrawMode(selected.value);
        window.Repaint();
        return;
    }

    const tile = hitTile(x, y);
    if (tile !== -1) {
        cycleTile(tile);
        window.Repaint();
    }
}

function on_script_unload() {
    if (paintTimer) clearInterval(paintTimer);
    vis.UpdateIntervalMs = INITIAL_ENGINE_INTERVAL;
}

setEngineInterval(engineInterval);
createDrawingResources();
for (const tile of tiles) createOrUpdateTile(tile, true);
setPaintRate(paintRate);