use std::time::Duration;
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
platform::windows::WindowExtWindows,
window::WindowBuilder,
};
use wry::WebViewBuilder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
std::fs::write("./index.html", html())?;
std::fs::write("./style.css", css())?;
std::fs::write("./script.js", js())?;
let mut html_dir = std::env::current_dir()?.into_os_string();
html_dir.push("/index.html");
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Black Hole")
.with_inner_size(tao::dpi::LogicalSize::new(1000.0, 700.0))
.build(&event_loop)?;
let _webview = WebViewBuilder::new()
.with_url(format!("file://{}", html_dir.to_string_lossy()))
.build(&window)?;
let hwnd = window.hwnd();
let event_forwarder =
wallopino::EventForwarder::new(hwnd, Some("Chrome_WidgetWin_1"), true, false)?;
event_forwarder.forward_events()?;
let mut attacher = wallopino::AttachWindow::auto_attach(hwnd, true)?;
attacher.start_watcher(Duration::from_millis(100))?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
_ => {}
}
});
}
fn html() -> &'static str {
r#"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Strings — Offline</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="container"></div>
<script src="script.js"></script>
</body>
</html>
"#
}
fn css() -> &'static str {
r##"body {
background: #49443c;
margin: 0;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
overflow: hidden;
}
canvas {
max-height: 100vh;
max-width: 100vw;
height: auto;
width: auto;
}
#container {
box-shadow: 0 0 20px rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
position: relative;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
}
h1 {
font-family: monospace;
font-size: 100px;
font-weight: 800;
line-height: 1em;
position: absolute;
color: #b39e86;
}
"##
}
fn js() -> &'static str {
r##"function lerp(a, b, t) {
return a + (b - a) * t;
}
function hash(value) {
let x = Number(value) | 0;
x = Math.imul(x ^ (x >>> 16), 0x45d9f3b);
x = Math.imul(x ^ (x >>> 16), 0x45d9f3b);
x = x ^ (x >>> 16);
return (x >>> 0) / 4294967296;
}
function getPointID(row, column, gridH) {
return column * gridH + row;
}
function getPointsForGridId(gridId, gridW, gridH) {
const row = gridId % gridH;
const column = Math.floor(gridId / gridH);
return { row, column };
}
function getEdgeIdsForGridId(gridId, gridW, gridH) {
const { row, column } = getPointsForGridId(gridId, gridW, gridH);
const ids = [];
if (column > 0) ids.push(getPointID(row, column - 1, gridH));
if (column < gridW - 1) ids.push(getPointID(row, column + 1, gridH));
if (row > 0) ids.push(getPointID(row - 1, column, gridH));
if (row < gridH - 1) ids.push(getPointID(row + 1, column, gridH));
return ids;
}
function smoothstep(edge0, edge1, x) {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
let fullCode = '';
const w = Math.min(960, Math.max(640, window.innerWidth - 120));
const h = Math.min(720, Math.max(480, window.innerHeight - 120));
// Use the display's native pixel ratio for a crisp curtain on high-DPI screens.
const dpr = window.devicePixelRatio || 1;
const CONFIG = {
awidth: w / 1.2,
aheight: h / 1.2,
// A denser grid keeps the project description readable while giving the
// curtain a larger physical footprint.
gridW: 34,
gridH: 34,
gravity: .2,
damping: .99,
iterationsPerFrame: 5,
compressFactor: .02,
stretchFactor: 1.1,
mouseSize: 5000,
mouseStrength: 5,
contain: false,
randomSolve: false,
preset: ''
};
CONFIG.cellWidth = CONFIG.awidth / (CONFIG.gridW - 1);
CONFIG.cellHeight = CONFIG.aheight / (CONFIG.gridH - 1);
function sizeCanvas() {
if (!c) return;
c.style.width = window.innerWidth + 'px';
c.style.height = window.innerHeight + 'px';
c.width = Math.round(window.innerWidth * dpr);
c.height = Math.round(window.innerHeight * dpr);
}
window.addEventListener('resize', () => {
CONFIG.awidth = Math.min(960, Math.max(640, window.innerWidth - 120));
CONFIG.aheight = Math.min(720, Math.max(480, window.innerHeight - 120));
CONFIG.cellWidth = CONFIG.awidth / (CONFIG.gridW - 1);
CONFIG.cellHeight = CONFIG.aheight / (CONFIG.gridH - 1);
if (c && c.width) sizeCanvas();
});
let rafID, input, c;
function main() {
if (rafID) cancelAnimationFrame(rafID);
if (input) input.unbind();
// The curtain renders this project description character-by-character.
// Keeping it here also makes the artwork self-contained and fully offline.
fullCode = ` A native Windows library for attaching arbitrary application windows to the desktop background, placing them behind the desktop icons while preserving their original window behavior and interaction.
It provides a practical foundation for building interactive desktop backgrounds, live wallpapers, embedded visualizations, and other experiences where a normal native window needs to behave like part of the Windows desktop.
The library explores and manages the Windows desktop window hierarchy, discovers the appropriate WorkerW and Shell desktop surfaces, and positions the target HWND within the desktop background layer without requiring the application itself to become a traditional wallpaper renderer.
Designed with Rust and the Win32 API, the project focuses on precise window topology, reliable interaction, and compatibility with real native windows and WebView2-based content. It can inspect HWND relationships, locate the desktop rendering surface, and attach a target window behind the desktop icons while keeping the rest of the Windows shell intact.
\\(*_*)/ --YAY FINALLY-- \\(*_*)/`;
const {
awidth: width,
aheight: height,
gridW,
gridH,
gravity,
damping,
iterationsPerFrame,
compressFactor,
stretchFactor,
cellWidth,
cellHeight
} = CONFIG;
// Character atlas.
const charCanvases = {};
const fontSize = Math.max(14, cellHeight * 1.05);
const box = Math.ceil(fontSize * 1.35);
for (const ch of new Set(fullCode)) {
if (ch === ' ') continue;
const off = document.createElement('canvas');
off.width = off.height = box * dpr;
const octx = off.getContext('2d');
octx.scale(dpr, dpr);
octx.font = `bold ${fontSize}px monospace`;
octx.textAlign = 'center';
octx.textBaseline = 'middle';
octx.fillStyle = '#d8c4ad';
octx.fillText(ch, box / 2, box / 2);
off.logicalSize = box;
charCanvases[ch] = off;
}
c = document.createElement('canvas');
container.innerHTML = '';
container.appendChild(c);
sizeCanvas();
const ctx = c.getContext('2d');
const particles = [];
const constraints = [];
const verticalConstraints = [];
const horizontalConstraints = [];
const pinnedParticles = [];
input = new Input({ c, particles });
for (let i = 0; i < gridW; i++) {
for (let j = 0; j < gridH; j++) {
const x = i * cellWidth;
const y = j * cellHeight;
const id = getPointID(j, i, gridH);
const pinned = j === 0;
const charIndex = (i + j * gridW) % fullCode.length;
const char = fullCode[charIndex] || ' ';
const particle = new Particle({ x, y, pinned, id, char });
particles.push(particle);
if (pinned) pinnedParticles.push(particle);
}
}
for (let i = 0; i < gridW; i++) {
for (let j = 0; j < gridH; j++) {
const id = getPointID(j, i, gridH);
const p = particles[id];
if (j < gridH - 1) {
const bottomP = particles[getPointID(j + 1, i, gridH)];
const constraint = new Constraint({
p1: p,
p2: bottomP,
length: cellHeight,
id: id + gridW * gridH,
compressFactor,
stretchFactor
});
constraints.push(constraint);
verticalConstraints.push(constraint);
p.downConstraint = constraint;
}
if (i < gridW - 1) {
const rightP = particles[getPointID(j, i + 1, gridH)];
const horizontal = new Constraint({
p1: p,
p2: rightP,
length: cellWidth,
id: id + gridW * gridH * 2,
compressFactor: 0.6,
stretchFactor: 4,
isSpacer: true
});
constraints.push(horizontal);
horizontalConstraints.push(horizontal);
}
}
}
function drawParticles() {
particles.forEach((p) => {
ctx.beginPath();
ctx.arc(...p.pos, 2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
});
}
function drawCode() {
const offsetX = (c.width / dpr - width) / 2;
const offsetY = (c.height / dpr - height) / 2 - 30;
particles.forEach((p) => {
if (!p.char || p.char === ' ') return;
const img = charCanvases[p.char];
if (!img) return;
let cos = 1;
let sin = 0;
const constraint = p.downConstraint;
if (constraint) {
const dx = constraint.p2.pos.x - constraint.p1.pos.x;
const dy = constraint.p2.pos.y - constraint.p1.pos.y;
const angle = Math.atan2(dy, dx) - Math.PI / 2;
cos = Math.cos(angle);
sin = Math.sin(angle);
}
const tx = p.pos.x + offsetX;
const ty = p.pos.y + offsetY;
ctx.setTransform(
dpr * cos,
dpr * sin,
-dpr * sin,
dpr * cos,
dpr * tx,
dpr * ty
);
const half = img.logicalSize / 2;
ctx.drawImage(img, -half, -half, img.logicalSize, img.logicalSize);
});
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
let lastDelta = 0;
function runloop(delta) {
rafID = requestAnimationFrame(runloop);
ctx.save();
ctx.clearRect(0, 0, c.width, c.height);
// requestAnimationFrame's timestamp is milliseconds; the original Pen
// uses this directly, so keep that behavior for fidelity.
particles.forEach((p) => p.update(delta - lastDelta));
lastDelta = delta;
if (CONFIG.randomSolve) shuffleArray(constraints);
for (let i = 0; i < iterationsPerFrame; i++) {
for (let j = 0; j < constraints.length; j++) constraints[j].solve();
}
if (CONFIG.contain) particles.forEach((p) => p.contain());
drawCode();
ctx.restore();
}
rafID = requestAnimationFrame(runloop);
}
class Input {
constructor({ c, particles }) {
this.c = c;
this.particles = particles;
this.mousePos = new Vec2();
this.grabRadius = 20;
this.grabbedParticle = null;
this.pointerIsDown = false;
this.activePointerId = null;
this.pointerUpTimer = null;
this.bound = false;
this.bind();
}
setMouse(e) {
const rect = this.c.getBoundingClientRect();
const cssX = e.clientX - rect.left;
const cssY = e.clientY - rect.top;
const offsetX = (this.c.width / dpr - CONFIG.awidth) / 2;
const offsetY = (this.c.height / dpr - CONFIG.aheight) / 2 - 30;
this.mousePos.x = cssX - offsetX;
this.mousePos.y = cssY - offsetY;
}
startPointerTracking(e) {
this.activePointerId = e.pointerId ?? null;
this.pointerIsDown = true;
// Pointer capture keeps the drag stream attached to the canvas even if
// the cursor leaves its bounds. This is especially important for a
// WebView2/composition wallpaper where pointerup may otherwise be lost.
if (e.pointerId != null && typeof this.c.setPointerCapture === 'function') {
try {
this.c.setPointerCapture(e.pointerId);
} catch (_) {
// Some hosts can reject capture; the fallback release paths below
// still make the interaction recoverable.
}
}
}
releasePointer(e = null) {
if (this.grabbedParticle) {
this.grabbedParticle.pinned =
this.grabbedParticle.originalPinnedState ?? false;
this.grabbedParticle.originalPinnedState = undefined;
this.grabbedParticle = null;
}
this.pointerIsDown = false;
const pointerId = e?.pointerId ?? this.activePointerId;
if (
pointerId != null &&
typeof this.c.hasPointerCapture === 'function' &&
typeof this.c.releasePointerCapture === 'function'
) {
try {
if (this.c.hasPointerCapture(pointerId)) {
this.c.releasePointerCapture(pointerId);
}
} catch (_) {
// Ignore host-specific capture errors.
}
}
this.activePointerId = null;
if (this.pointerUpTimer) {
clearTimeout(this.pointerUpTimer);
this.pointerUpTimer = null;
}
}
pointerdown(e) {
// Ignore secondary pointers while a drag is already active.
if (this.activePointerId != null && e.pointerId !== this.activePointerId) {
return;
}
this.setMouse(e);
this.startPointerTracking(e);
// A fresh pointerdown must never inherit a stale grabbed particle.
if (this.grabbedParticle) {
this.releasePointer(e);
}
this.startPointerTracking(e);
for (const p of this.particles) {
if (this.mousePos.subtractNew(p.pos).length < this.grabRadius) {
this.grabbedParticle = p;
this.grabbedParticle.originalPinnedState = this.grabbedParticle.pinned;
this.grabbedParticle.pinned = true;
break;
}
}
}
pointerup(e) {
if (this.activePointerId != null && e.pointerId !== this.activePointerId) {
return;
}
this.releasePointer(e);
}
pointercancel(e) {
if (this.activePointerId != null && e.pointerId !== this.activePointerId) {
return;
}
this.releasePointer(e);
}
lostpointercapture(e) {
if (this.activePointerId != null && e.pointerId !== this.activePointerId) {
return;
}
this.releasePointer(e);
}
pointermove(e) {
if (this.activePointerId != null && e.pointerId !== this.activePointerId) {
return;
}
// If the host tells us no mouse button is currently held, treat that as
// an implicit release. This catches hosts that drop pointerup but still
// deliver a later pointermove.
if (this.activePointerId != null && typeof e.buttons === 'number' && e.buttons === 0) {
this.releasePointer(e);
return;
}
this.setMouse(e);
if (this.grabbedParticle) {
this.grabbedParticle.pos.reset(this.mousePos.x, this.mousePos.y);
this.grabbedParticle.oldPos.reset(this.mousePos.x, this.mousePos.y);
}
for (const p of this.particles) {
const diff = this.mousePos.subtractNew(p.pos);
const ls = diff.lengthSquared;
if (ls < CONFIG.mouseSize) {
const a = diff.angle - Math.PI;
const strength = smoothstep(CONFIG.mouseSize, -2000, ls) * CONFIG.mouseStrength / 300;
const force = new Vec2(Math.cos(a) * strength, Math.sin(a) * strength);
p.applyForce(force);
}
}
}
contextmenu(e) {
e.preventDefault();
}
windowblur() {
this.releasePointer();
}
visibilitychange() {
if (document.hidden) {
this.releasePointer();
}
}
bind() {
this.pointerdown = this.pointerdown.bind(this);
this.pointerup = this.pointerup.bind(this);
this.pointercancel = this.pointercancel.bind(this);
this.pointermove = this.pointermove.bind(this);
this.lostpointercapture = this.lostpointercapture.bind(this);
this.contextmenu = this.contextmenu.bind(this);
this.windowblur = this.windowblur.bind(this);
this.visibilitychange = this.visibilitychange.bind(this);
document.addEventListener('pointerdown', this.pointerdown);
document.addEventListener('pointerup', this.pointerup);
document.addEventListener('pointercancel', this.pointercancel);
document.addEventListener('pointermove', this.pointermove);
document.addEventListener('lostpointercapture', this.lostpointercapture);
document.addEventListener('contextmenu', this.contextmenu);
window.addEventListener('blur', this.windowblur);
document.addEventListener('visibilitychange', this.visibilitychange);
this.bound = true;
}
unbind() {
// Always clean up an active drag before removing listeners.
this.releasePointer();
if (!this.bound) return;
document.removeEventListener('pointerdown', this.pointerdown);
document.removeEventListener('pointerup', this.pointerup);
document.removeEventListener('pointercancel', this.pointercancel);
document.removeEventListener('pointermove', this.pointermove);
document.removeEventListener('lostpointercapture', this.lostpointercapture);
document.removeEventListener('contextmenu', this.contextmenu);
window.removeEventListener('blur', this.windowblur);
document.removeEventListener('visibilitychange', this.visibilitychange);
this.bound = false;
}
}
class Vec2 {
constructor(x = 0, y = 0) {
this.reset(x, y);
}
zero() {
this.reset(0, 0);
}
reset(x = 0, y = 0) {
this.x = x;
this.y = y;
}
clone() {
return new Vec2(this.x, this.y);
}
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
addNew(v) {
return this.clone().add(v);
}
subtract(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
subtractNew(v) {
return this.clone().subtract(v);
}
multiply(v) {
this.x *= v.x;
this.y *= v.y;
return this;
}
multiplyNew(v) {
return this.clone().multiply(v);
}
scale(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
scaleNew(scalar) {
return this.clone().scale(scalar);
}
get array() {
return [this.x, this.y];
}
get lengthSquared() {
return this.x ** 2 + this.y ** 2;
}
get length() {
return Math.hypot(this.x, this.y);
}
get angle() {
return Math.atan2(this.y, this.x);
}
[Symbol.iterator]() {
const values = this.array;
let i = 0;
return {
next() {
if (i < values.length) return { value: values[i++], done: false };
return { done: true };
}
};
}
}
class Particle {
constructor({ x, y, pinned, id, char } = {}) {
this.pos = new Vec2(x, y);
this.oldPos = new Vec2(x, y);
this.velocity = new Vec2();
this.acceleration = new Vec2();
this.pinned = pinned;
this.id = id;
this.char = char;
this.gravityVec = new Vec2();
}
contain() {
if (this.pinned) return;
const radius = 5;
if (this.pos.x < radius) {
this.pos.x = radius;
this.oldPos.x = this.pos.x + Math.abs(this.oldPos.x - this.pos.x) * 0.8;
} else if (this.pos.x > CONFIG.awidth - radius) {
this.pos.x = CONFIG.awidth - radius;
this.oldPos.x = this.pos.x - Math.abs(this.oldPos.x - this.pos.x) * 0.8;
}
if (this.pos.y < radius) {
this.pos.y = radius;
this.oldPos.y = this.pos.y + Math.abs(this.oldPos.y - this.pos.y) * 0.8;
} else if (this.pos.y > CONFIG.aheight - radius) {
this.pos.y = CONFIG.aheight - radius;
this.oldPos.y = this.pos.y - Math.abs(this.oldPos.y - this.pos.y) * 0.8;
}
}
update(delta) {
if (this.pinned) {
this.acceleration.zero();
return;
}
this.velocity.reset(
(this.pos.x - this.oldPos.x) * CONFIG.damping,
(this.pos.y - this.oldPos.y) * CONFIG.damping
);
this.oldPos.reset(...this.pos);
// Guard the first frame against a zero/near-zero delta.
const safeDelta = Math.max(delta, 0.001);
const dd = safeDelta ** 2;
this.gravityVec.reset(0, CONFIG.gravity / dd);
this.applyForce(this.gravityVec);
this.pos.x += this.velocity.x + this.acceleration.x * dd;
this.pos.y += this.velocity.y + this.acceleration.y * dd;
this.acceleration.reset();
}
applyForce(v) {
this.acceleration.add(v);
}
}
class Constraint {
constructor({ p1, p2, length, id, compressFactor, stretchFactor, isSpacer }) {
this.p1 = p1;
this.p2 = p2;
this.length = length;
this.id = id;
this.isSpacer = !!isSpacer;
this.minLength = length * compressFactor;
this.maxLength = length * stretchFactor;
this.compressFactor = compressFactor;
this.stretchFactor = stretchFactor;
c.addEventListener('update', (e) => {
const detail = e.detail || {};
const minFactor = this.isSpacer ? this.compressFactor : (detail.compressFactor ?? this.compressFactor);
const maxFactor = this.isSpacer ? this.stretchFactor : (detail.stretchFactor ?? this.stretchFactor);
this.minLength = this.length * minFactor;
this.maxLength = this.length * maxFactor;
});
}
solve() {
const dx = this.p2.pos.x - this.p1.pos.x;
const dy = this.p2.pos.y - this.p1.pos.y;
const distance = Math.hypot(dx, dy);
if (distance === 0) return;
let targetLength = this.length;
if (distance < this.minLength) targetLength = this.minLength;
else if (distance > this.maxLength) targetLength = this.maxLength;
else return;
const difference = targetLength - distance;
const percent = difference / distance / 2;
const offsetX = dx * percent;
const offsetY = dy * percent;
if (!this.p1.pinned) {
this.p1.pos.x -= offsetX;
this.p1.pos.y -= offsetY;
}
if (!this.p2.pinned) {
this.p2.pos.x += offsetX;
this.p2.pos.y += offsetY;
}
}
}
setTimeout(() => main(), 500);
"##
}