import {
Application,
Container,
Graphics,
Sprite,
Text,
TextStyle,
Texture,
} from "pixi.js";
async function setup(app: Application) {
await app.init({ background: "#111111", resizeTo: window, antialias: true });
document.getElementById("pixi-container")!.appendChild(app.canvas);
}
function makeColorBlock(color: number): Container {
const container = new Container();
const rect = new Graphics().rect(0, 0, 400, 400).fill(color);
container.addChild(rect);
return container;
}
interface Unit {
x: number;
y: number;
vx: number;
vy: number;
}
function stepUnit(u: Unit, dt: number, bounds: number, radius: number) {
u.x += u.vx * dt;
u.y += u.vy * dt;
if (u.x < radius && u.vx < 0) {
u.x = radius;
u.vx *= -1;
} else if (u.x > bounds - radius && u.vx > 0) {
u.x = bounds - radius;
u.vx *= -1;
}
if (u.y < radius && u.vy < 0) {
u.y = radius;
u.vy *= -1;
} else if (u.y > bounds - radius && u.vy > 0) {
u.y = bounds - radius;
u.vy *= -1;
}
}
function makeCosSqMaskTexture(
size: number,
nominalRadius: number,
edgeWidth: number,
): Texture {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;
const imageData = ctx.createImageData(size, size);
const cx = size / 2;
const cy = size / 2;
const inner = nominalRadius - edgeWidth / 2;
const outer = nominalRadius + edgeWidth / 2;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const r = Math.hypot(x - cx, y - cy);
let v: number;
if (r <= inner) {
v = 1;
} else if (r >= outer) {
v = 0;
} else {
const t = (r - inner) / edgeWidth;
const c = Math.cos((Math.PI / 2) * t);
v = c * c;
}
const i = (y * size + x) * 4;
const byte = Math.round(v * 255);
imageData.data[i] = byte;
imageData.data[i + 1] = byte;
imageData.data[i + 2] = byte;
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
return Texture.from(canvas);
}
function addGrid(container: Container, size: number, interval: number) {
const grid = new Graphics();
grid.setStrokeStyle({ color: 0x808080, width: 1 });
for (let x = interval; x < size; x += interval) {
grid.moveTo(x, 0).lineTo(x, size);
}
for (let y = interval; y < size; y += interval) {
grid.moveTo(0, y).lineTo(size, y);
}
grid.stroke();
container.addChild(grid);
}
export async function main() {
const app = new Application();
await setup(app);
const labelStyle = new TextStyle({
fill: 0x666666,
fontSize: 14,
fontFamily: "monospace",
});
const red = makeColorBlock(0xff0000);
const green = makeColorBlock(0x00ff00);
const blue = makeColorBlock(0x0000ff);
const layouts = (["Scissors", "Stencil", "Alpha"] as const).map(
(label, i) => {
const block = [red, green, blue][i];
const layout = new Container();
layout.addChild(new Text({ text: label, style: labelStyle }));
block.position.set(0, 20);
layout.addChild(block);
app.stage.addChild(layout);
return layout;
},
);
const updateLayout = () => {
const isLandscape = window.innerWidth > window.innerHeight;
layouts.forEach((layout, i) => {
layout.position.set(
isLandscape ? 50 + i * 450 : 50,
isLandscape ? 50 : 30 + i * 450,
);
});
};
updateLayout();
app.renderer.on("resize", updateLayout);
addGrid(red, 400, 50);
addGrid(green, 400, 50);
addGrid(blue, 400, 50);
const scissorMask = new Graphics().rect(-100, -100, 200, 200).fill(0xffffff);
red.addChild(scissorMask);
red.mask = scissorMask;
const greenSideContainer = new Container();
green.addChild(greenSideContainer);
green.mask = greenSideContainer;
const blueMaskTexture = makeCosSqMaskTexture(400, 100, 50);
const blueMaskContainer = new Container();
const blueMaskSprite = new Sprite(blueMaskTexture);
blueMaskSprite.position.set(-200, -200);
blueMaskContainer.addChild(blueMaskSprite);
blue.addChild(blueMaskContainer);
blue.mask = blueMaskSprite;
const unitRadius = 10;
const blueUnit: Unit = { x: 200, y: 200, vx: -2.1, vy: 1.6 };
const blueUnitGraphic = new Graphics()
.circle(0, 0, unitRadius)
.fill(0xffffff);
blueUnitGraphic.position.set(blueUnit.x, blueUnit.y);
blue.addChild(blueUnitGraphic);
const redUnit: Unit = { x: 200, y: 200, vx: 1.7, vy: -2.3 };
const redUnitGraphic = new Graphics().circle(0, 0, unitRadius).fill(0xffffff);
redUnitGraphic.position.set(redUnit.x, redUnit.y);
red.addChild(redUnitGraphic);
const greenUnits: Unit[] = [
{ x: 190, y: 210, vx: -0.8, vy: -2.9 },
{ x: 210, y: 210, vx: 1.3, vy: -2.5 },
];
const unitGraphics = greenUnits.map((u) => {
const g = new Graphics().circle(0, 0, unitRadius).fill(0xffffff);
g.position.set(u.x, u.y);
green.addChild(g);
return g;
});
const followerCircles = greenUnits.map((u) => {
const g = new Graphics().circle(0, 0, 100).fill(0xffffff);
g.position.set(u.x, u.y);
greenSideContainer.addChild(g);
return g;
});
app.ticker.add((ticker) => {
const dt = ticker.deltaTime;
stepUnit(blueUnit, dt, 400, unitRadius);
blueUnitGraphic.position.set(blueUnit.x, blueUnit.y);
blueMaskContainer.position.set(blueUnit.x, blueUnit.y);
stepUnit(redUnit, dt, 400, unitRadius);
redUnitGraphic.position.set(redUnit.x, redUnit.y);
scissorMask.position.set(redUnit.x, redUnit.y);
for (let i = 0; i < greenUnits.length; i++) {
stepUnit(greenUnits[i], dt, 400, unitRadius);
unitGraphics[i].position.set(greenUnits[i].x, greenUnits[i].y);
followerCircles[i].position.set(greenUnits[i].x, greenUnits[i].y);
}
});
}