Documentation/Source example
Example source
samples/worker/03. Fractal Renderer.js
Referenced by:Worker
'use strict';
// Worker sample 3: Fractal Renderer
// Demonstrates: CPU-bound Worker work, stable local pan/zoom preview,
// settled-view rendering, stale-frame rejection and transferable bitmap frames.
// The in-panel Renderer switch recreates the Worker so the same gdi.* facade can be
// compared in GDI+ and Direct2D modes.
let drawMode = 1;
window.DrawMode = drawMode;
const DT_LEFT = 0x0000;
const DT_CENTER = 0x0001;
const DT_VCENTER = 0x0004;
const DT_SINGLELINE = 0x0020;
const DT_NOPREFIX = 0x0800;
const VIEW_DEBOUNCE_MS = 80;
const MAX_ITERATIONS = 220;
const PREVIEW_INTERPOLATION = 2; // InterpolationMode.HighQuality
let INFO_FONT = gdi.Font('Segoe UI', 13);
let switchingRenderer = false;
const DEFAULT_VIEW = { centerX: -0.65, centerY: 0.0, scale: 2.8 };
let frame = null;
let frameView = null;
let previewFrame = null;
let previewView = null;
let previewActive = false;
let view = { ...DEFAULT_VIEW };
let viewId = 0;
let viewDirty = true;
let mouseX = 0;
let mouseY = 0;
let drag = null;
let wheelActive = false;
let sendTimer = 0;
const workerSource = String.raw`
'use strict';
function getPalette(maxIterations) {
const palette = new Uint32Array(maxIterations + 1);
for (let iteration = 0; iteration < maxIterations; ++iteration) {
const t = iteration / maxIterations;
const r = Math.floor(255 * Math.min(1, 9 * (1 - t) * t * t * t));
const g = Math.floor(255 * Math.min(1, 15 * (1 - t) * (1 - t) * t * t));
const b = Math.floor(255 * Math.min(1, 8.5 * (1 - t) * (1 - t) * (1 - t) * t));
palette[iteration] = (0xFF000000 | (r << 16) | (g << 8) | b) >>> 0;
}
palette[maxIterations] = 0xFF0C0808;
return palette;
}
const palettes = new Map();
function paletteFor(maxIterations) {
let palette = palettes.get(maxIterations);
if (!palette) {
palette = getPalette(maxIterations);
palettes.set(maxIterations, palette);
}
return palette;
}
function createRenderJob(width, height, centerX, centerY, scale, maxIterations) {
const pixels = new Uint8Array(width * height * 4);
return {
width,
height,
centerX,
centerY,
scale,
maxIterations,
pixels,
pixel32: new Uint32Array(pixels.buffer),
palette: paletteFor(maxIterations),
xStep: scale * (width / Math.max(1, height)) / width,
yStep: scale / height,
left: centerX - scale * (width / Math.max(1, height)) * 0.5,
top: centerY - scale * 0.5,
y: 0
};
}
function renderRow(job, y) {
const cy = job.top + y * job.yStep;
const row = y * job.width;
for (let x = 0; x < job.width; ++x) {
const cx = job.left + x * job.xStep;
const xm = cx - 0.25;
const q = xm * xm + cy * cy;
let iteration = job.maxIterations;
// Main cardioid and period-2 bulb are known to be inside the set.
if (q * (q + xm) > 0.25 * cy * cy && (cx + 1) * (cx + 1) + cy * cy > 0.0625) {
let zx = 0;
let zy = 0;
let zx2 = 0;
let zy2 = 0;
iteration = 0;
while (zx2 + zy2 <= 4 && iteration < job.maxIterations) {
zy = 2 * zx * zy + cy;
zx = zx2 - zy2 + cx;
zx2 = zx * zx;
zy2 = zy * zy;
++iteration;
}
}
job.pixel32[row + x] = job.palette[iteration];
}
}
function finishBitmap(job) {
// In DrawMode 0 this is a GdiBitmap; in DrawMode 1 the same gdi.* call
// is routed to the Worker's Direct2D backend and returns a D2DBitmap.
return gdi.CreateImageFromPixelData(job.pixels, job.width, job.height, 'bgra32');
}
function renderBitmap(width, height, centerX, centerY, scale, maxIterations) {
const job = createRenderJob(width, height, centerX, centerY, scale, maxIterations);
for (; job.y < job.height; ++job.y) renderRow(job, job.y);
return finishBitmap(job);
}
onmessage = function (event) {
const current = event.data;
const bitmap = renderBitmap(
current.width,
current.height,
current.centerX,
current.centerY,
current.scale,
current.maxIterations
);
if (bitmap) {
postMessage({
viewId: current.viewId,
width: current.width,
height: current.height,
centerX: current.centerX,
centerY: current.centerY,
scale: current.scale,
bitmap
}, [bitmap]);
}
};
`;
let worker = null;
function startWorker() {
switchingRenderer = true;
if (worker) worker.terminate();
frame = null;
frameView = null;
previewFrame = null;
previewView = null;
previewActive = false;
wheelActive = false;
if (sendTimer) {
clearTimeout(sendTimer);
sendTimer = 0;
}
try {
window.DrawMode = drawMode;
INFO_FONT = gdi.Font('Segoe UI', 13);
} finally {
switchingRenderer = false;
}
const currentWorker = new Worker(workerSource, 'fractal-renderer');
worker = currentWorker;
currentWorker.onmessage = function (event) {
const data = event.data;
if (worker !== currentWorker || data.viewId !== viewId || drag || wheelActive) return;
frame = data.bitmap;
frameView = {
width: data.width,
height: data.height,
centerX: data.centerX,
centerY: data.centerY,
scale: data.scale
};
previewFrame = null;
previewView = null;
previewActive = false;
window.Repaint();
};
currentWorker.onerror = function (event) {
if (worker === currentWorker) console.log(`Worker error: ${event.message}`);
};
invalidateView();
sendView();
window.Repaint();
}
function invalidateView() {
++viewId;
viewDirty = true;
}
function sendView() {
sendTimer = 0;
if (!viewDirty || window.Width <= 0 || window.Height <= 0 || !worker) return;
viewDirty = false;
worker.postMessage({
type: 'render',
viewId,
width: window.Width,
height: window.Height,
centerX: view.centerX,
centerY: view.centerY,
scale: view.scale,
maxIterations: MAX_ITERATIONS
});
}
function queueView() {
if (sendTimer) clearTimeout(sendTimer);
sendTimer = setTimeout(function () {
sendTimer = 0;
wheelActive = false;
if (!drag) sendView();
}, VIEW_DEBOUNCE_MS);
}
function flushView() {
if (sendTimer) {
clearTimeout(sendTimer);
sendTimer = 0;
}
wheelActive = false;
sendView();
}
function screenToWorld(x, y, currentView = view) {
const width = Math.max(1, window.Width);
const height = Math.max(1, window.Height);
const aspect = width / height;
return {
x: currentView.centerX + (x / width - 0.5) * currentView.scale * aspect,
y: currentView.centerY + (y / height - 0.5) * currentView.scale
};
}
function frameSlice(sourceView) {
if (!sourceView || window.Width <= 0 || window.Height <= 0) return null;
const viewAspect = window.Width / window.Height;
const viewWorldW = view.scale * viewAspect;
const viewLeft = view.centerX - viewWorldW * 0.5;
const viewTop = view.centerY - view.scale * 0.5;
const viewRight = viewLeft + viewWorldW;
const viewBottom = viewTop + view.scale;
const frameAspect = sourceView.width / Math.max(1, sourceView.height);
const frameWorldW = sourceView.scale * frameAspect;
const frameLeft = sourceView.centerX - frameWorldW * 0.5;
const frameTop = sourceView.centerY - sourceView.scale * 0.5;
const frameRight = frameLeft + frameWorldW;
const frameBottom = frameTop + sourceView.scale;
const left = Math.max(viewLeft, frameLeft);
const top = Math.max(viewTop, frameTop);
const right = Math.min(viewRight, frameRight);
const bottom = Math.min(viewBottom, frameBottom);
if (right <= left || bottom <= top) return null;
return {
sx: (left - frameLeft) / frameWorldW * sourceView.width,
sy: (top - frameTop) / sourceView.scale * sourceView.height,
sw: (right - left) / frameWorldW * sourceView.width,
sh: (bottom - top) / sourceView.scale * sourceView.height,
dx: (left - viewLeft) / viewWorldW * window.Width,
dy: (top - viewTop) / view.scale * window.Height,
dw: (right - left) / viewWorldW * window.Width,
dh: (bottom - top) / view.scale * window.Height
};
}
function beginPreview() {
if (previewActive) return;
previewFrame = frame;
previewView = frameView;
previewActive = !!previewFrame;
}
function on_size() {
beginPreview();
invalidateView();
window.Repaint();
queueView();
}
function on_mouse_move(x, y) {
mouseX = x;
mouseY = y;
if (!drag || window.Width <= 0 || window.Height <= 0) return;
const aspect = window.Width / Math.max(1, window.Height);
view.centerX = drag.centerX - (x - drag.x) / window.Width * drag.scale * aspect;
view.centerY = drag.centerY - (y - drag.y) / window.Height * drag.scale;
invalidateView();
// Interaction is immediate and uses one stable completed bitmap for the
// entire drag. The Worker is not fed intermediate drag positions.
window.Repaint();
}
const RENDERER_GDI = { x: 82, y: 8, w: 46, h: 26 };
const RENDERER_D2D = { x: 132, y: 8, w: 52, h: 26 };
function contains(rect, x, y) {
return x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h;
}
function on_mouse_lbtn_down(x, y) {
if (contains(RENDERER_GDI, x, y) || contains(RENDERER_D2D, x, y)) return;
mouseX = x;
mouseY = y;
beginPreview();
drag = {
x,
y,
centerX: view.centerX,
centerY: view.centerY,
scale: view.scale
};
}
function on_mouse_lbtn_up(x, y) {
if (contains(RENDERER_GDI, x, y) || contains(RENDERER_D2D, x, y)) {
const nextMode = contains(RENDERER_D2D, x, y) ? 1 : 0;
drag = null;
if (nextMode !== drawMode) {
if (sendTimer) {
clearTimeout(sendTimer);
sendTimer = 0;
}
drawMode = nextMode;
startWorker();
}
return;
}
if (!drag) return;
drag = null;
if (viewDirty) flushView();
}
function on_mouse_wheel(step) {
if (window.Width <= 0 || window.Height <= 0 || !step) return;
if (!wheelActive) beginPreview();
const before = screenToWorld(mouseX, mouseY);
const factor = Math.pow(0.82, step);
view.scale = Math.max(1e-12, Math.min(4.0, view.scale * factor));
const after = screenToWorld(mouseX, mouseY);
view.centerX += before.x - after.x;
view.centerY += before.y - after.y;
wheelActive = true;
invalidateView();
// Zoom one stable completed frame locally for the whole wheel burst.
// Only the final viewport is rendered after the burst settles.
window.Repaint();
queueView();
}
function on_mouse_rbtn_up() {
beginPreview();
view = { ...DEFAULT_VIEW };
drag = null;
wheelActive = false;
invalidateView();
window.Repaint();
flushView();
return true;
}
function drawChoice(gr, rect, text, selected) {
gr.FillSolidRect(rect.x, rect.y, rect.w, rect.h, selected ? 0xFF355D7A : 0xFF20262E);
gr.DrawText(text, INFO_FONT, selected ? 0xFFFFFFFF : 0xFFB7C1CB,
rect.x, rect.y, rect.w, rect.h, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
}
function on_paint(gr) {
gr.FillSolidRect(0, 0, window.Width, window.Height, 0xFF101014);
if (switchingRenderer) return;
const shownFrame = previewActive && previewFrame ? previewFrame : frame;
const shownView = previewActive && previewFrame ? previewView : frameView;
if (shownFrame) {
const slice = frameSlice(shownView);
if (slice) {
gr.SetInterpolationMode(PREVIEW_INTERPOLATION);
gr.DrawImage(shownFrame, slice.dx, slice.dy, slice.dw, slice.dh, slice.sx, slice.sy, slice.sw, slice.sh);
}
}
gr.FillSolidRect(8, 8, Math.max(1, Math.min(window.Width - 16, 520)), 26, 0xC0101318);
gr.DrawText('Renderer:', INFO_FONT, 0xFFB7C1CB, 14, 8, 66, 26,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
drawChoice(gr, RENDERER_GDI, 'GDI', drawMode === 0);
drawChoice(gr, RENDERER_D2D, 'D2D', drawMode === 1);
gr.DrawText('Wheel: zoom Drag: pan Right click: reset', INFO_FONT, 0xFFE8EDF2,
196, 8, Math.max(1, window.Width - 210), 26, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
}
startWorker();