use crate::noise::NoiseEngine;
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn canvas_noise_script(engine: &NoiseEngine) -> String {
let noise_fn = engine.js_noise_fn();
format!(
r"(function() {{
'use strict';
// ── Noise helpers (injected from T37 NoiseEngine) ──────────────────────
{noise_fn}
// ── Utility: apply pixel noise to an ImageData in-place ────────────────
function _applyCanvasNoise(imageData, offsetX, offsetY, operation) {{
const data = imageData.data;
const w = imageData.width;
for (let i = 0; i < data.length; i += 4) {{
const pixelIdx = (i / 4) | 0;
const px = (offsetX + (pixelIdx % w)) >>> 0;
const py = (offsetY + ((pixelIdx / w) | 0)) >>> 0;
// Only noise non-transparent, non-zero pixels to avoid noising blank canvases
if (data[i] === 0 && data[i | 1] === 0 && data[i | 2] === 0 && data[i | 3] === 0) {{
continue;
}}
const [dr, dg, db, da] = __stygian_noise(operation, px, py);
data[i] = Math.max(0, Math.min(255, data[i] + dr));
data[i | 1] = Math.max(0, Math.min(255, data[i | 1] + dg));
data[i | 2] = Math.max(0, Math.min(255, data[i | 2] + db));
data[i | 3] = Math.max(0, Math.min(255, data[i | 3] + da));
}}
return imageData;
}}
// ── Helper: copy canvas content to a temp canvas, noise it, return it ──
function _noisedCanvas(src) {{
const tmp = (typeof OffscreenCanvas !== 'undefined' && src instanceof OffscreenCanvas)
? new OffscreenCanvas(src.width, src.height)
: document.createElement('canvas');
tmp.width = src.width;
tmp.height = src.height;
const ctx = tmp.getContext('2d');
ctx.drawImage(src, 0, 0);
const id = ctx.getImageData(0, 0, tmp.width, tmp.height);
_applyCanvasNoise(id, 0, 0, 'canvas.toDataURL');
ctx.putImageData(id, 0, 0);
return tmp;
}}
// ── Spoof toString so overrides look like native functions ───────────────
function _nativeToString(name) {{
return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }};
}}
function _defineNative(obj, prop, fn) {{
fn.toString = _nativeToString(prop);
Object.defineProperty(obj, prop, {{
value: fn,
writable: false,
configurable: false,
enumerable: false,
}});
}}
// ── CanvasRenderingContext2D.getImageData ────────────────────────────────
(function() {{
const ctx2d = CanvasRenderingContext2D.prototype;
const _origGetImageData = ctx2d.getImageData;
_defineNative(ctx2d, 'getImageData', function getImageData(sx, sy, sw, sh, settings) {{
const id = settings !== undefined
? _origGetImageData.call(this, sx, sy, sw, sh, settings)
: _origGetImageData.call(this, sx, sy, sw, sh);
return _applyCanvasNoise(id, sx >>> 0, sy >>> 0, 'canvas.getImageData');
}});
}})();
// ── HTMLCanvasElement.toDataURL ──────────────────────────────────────────
(function() {{
if (typeof HTMLCanvasElement === 'undefined') return;
const _origToDataURL = HTMLCanvasElement.prototype.toDataURL;
_defineNative(HTMLCanvasElement.prototype, 'toDataURL', function toDataURL(type, quality) {{
const tmp = _noisedCanvas(this);
return quality !== undefined
? _origToDataURL.call(tmp, type, quality)
: type !== undefined
? _origToDataURL.call(tmp, type)
: _origToDataURL.call(tmp);
}});
}})();
// ── HTMLCanvasElement.toBlob ─────────────────────────────────────────────
(function() {{
if (typeof HTMLCanvasElement === 'undefined') return;
const _origToBlob = HTMLCanvasElement.prototype.toBlob;
_defineNative(HTMLCanvasElement.prototype, 'toBlob', function toBlob(callback, type, quality) {{
const tmp = _noisedCanvas(this);
if (quality !== undefined) {{
_origToBlob.call(tmp, callback, type, quality);
}} else if (type !== undefined) {{
_origToBlob.call(tmp, callback, type);
}} else {{
_origToBlob.call(tmp, callback);
}}
}});
}})();
// ── OffscreenCanvasRenderingContext2D.getImageData ───────────────────────
(function() {{
if (typeof OffscreenCanvasRenderingContext2D === 'undefined') return;
const octx2d = OffscreenCanvasRenderingContext2D.prototype;
const _origOGID = octx2d.getImageData;
_defineNative(octx2d, 'getImageData', function getImageData(sx, sy, sw, sh, settings) {{
const id = settings !== undefined
? _origOGID.call(this, sx, sy, sw, sh, settings)
: _origOGID.call(this, sx, sy, sw, sh);
return _applyCanvasNoise(id, sx >>> 0, sy >>> 0, 'offscreencanvas.getImageData');
}});
}})();
// ── OffscreenCanvas.convertToBlob ────────────────────────────────────────
(function() {{
if (typeof OffscreenCanvas === 'undefined') return;
const _origCTB = OffscreenCanvas.prototype.convertToBlob;
_defineNative(OffscreenCanvas.prototype, 'convertToBlob', async function convertToBlob(options) {{
const tmp = new OffscreenCanvas(this.width, this.height);
const ctx = tmp.getContext('2d');
ctx.drawImage(this, 0, 0);
const id = ctx.getImageData(0, 0, tmp.width, tmp.height);
_applyCanvasNoise(id, 0, 0, 'offscreencanvas.convertToBlob');
ctx.putImageData(id, 0, 0);
return options !== undefined
? _origCTB.call(tmp, options)
: _origCTB.call(tmp);
}});
}})();
}})();
"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::noise::{NoiseEngine, NoiseSeed};
fn engine(seed: u64) -> NoiseEngine {
NoiseEngine::new(NoiseSeed::from(seed))
}
#[test]
fn script_contains_seed() {
let js = canvas_noise_script(&engine(12345));
assert!(js.contains("12345"), "seed not embedded");
}
#[test]
fn script_contains_all_five_overrides() {
let js = canvas_noise_script(&engine(1));
assert!(js.contains("getImageData"), "missing getImageData");
assert!(js.contains("toDataURL"), "missing toDataURL");
assert!(js.contains("toBlob"), "missing toBlob");
assert!(
js.contains("OffscreenCanvasRenderingContext2D"),
"missing OffscreenCanvas getImageData"
);
assert!(js.contains("convertToBlob"), "missing convertToBlob");
}
#[test]
fn script_contains_native_tostring_spoofing() {
let js = canvas_noise_script(&engine(1));
assert!(
js.contains("[native code]"),
"missing native code toString spoof"
);
}
#[test]
fn script_contains_noise_helper() {
let js = canvas_noise_script(&engine(1));
assert!(js.contains("__stygian_noise"), "missing __stygian_noise");
}
#[test]
fn different_seeds_produce_different_seeds_in_script() {
let js1 = canvas_noise_script(&engine(111));
let js2 = canvas_noise_script(&engine(222));
assert_ne!(js1, js2, "scripts should differ for different seeds");
}
}