Skip to content

Instantly share code, notes, and snippets.

@jinjor
Last active May 7, 2023 18:31
Show Gist options
  • Save jinjor/a8c80a58e791facbe94b2c10d06b5990 to your computer and use it in GitHub Desktop.
Save jinjor/a8c80a58e791facbe94b2c10d06b5990 to your computer and use it in GitHub Desktop.
万華鏡
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.2.1/build/decomp.min.js"></script>
</head>
<body>
<div style="display: flex; gap: 10px">
<div id="world"></div>
<div id="view"></div>
</div>
<script>
const {
Engine,
Render,
Runner,
Bodies,
Composite,
Events,
Vertices,
Common,
} = Matter;
Common.setDecomp(decomp);
const engine = Engine.create();
const spinnerRadius = 120;
const radius = 60;
const viewWidth = 480;
const viewHeight = 480;
const ballRadius = 12;
const ballRadiusVar = 10;
const numBalls = 15;
const generation = 7;
const render = Render.create({
element: document.getElementById("world"),
engine: engine,
options: {
width: spinnerRadius * 2,
height: spinnerRadius * 2,
wireframes: false,
},
});
function posFromAngle(angle, radius) {
return { x: radius * Math.cos(angle), y: radius * Math.sin(angle) };
}
const spinner = Bodies.fromVertices(
0,
0,
[
posFromAngle((Math.PI / 6) * 1, spinnerRadius + 40),
posFromAngle((Math.PI / 6) * 5, spinnerRadius + 40),
posFromAngle((Math.PI / 6) * 9, spinnerRadius + 40),
posFromAngle((Math.PI / 6) * 1, spinnerRadius + 40),
posFromAngle((Math.PI / 6) * 1, spinnerRadius),
posFromAngle((Math.PI / 6) * 9, spinnerRadius),
posFromAngle((Math.PI / 6) * 5, spinnerRadius),
posFromAngle((Math.PI / 6) * 1, spinnerRadius),
],
{
isStatic: true,
render: { fillStyle: "#eea" },
}
);
const balls = [];
for (let i = 0; i < numBalls; i++) {
balls.push(
Bodies.circle(
0,
0,
ballRadius + (2 * Math.random() - 1) * ballRadiusVar
)
);
}
Composite.add(engine.world, balls);
Composite.add(engine.world, [spinner]);
Render.run(render);
Render.lookAt(render, {
min: { x: -spinnerRadius, y: -spinnerRadius },
max: { x: spinnerRadius, y: spinnerRadius },
});
const runner = Runner.create();
Runner.run(runner, engine);
setInterval(() => {
Matter.Body.rotate(spinner, 0.01);
const c = document.getElementById("world").children[0];
const ctx = c.getContext("2d");
const imgData = ctx.getImageData(
spinnerRadius - radius,
spinnerRadius - radius,
radius * 2,
radius * 2
);
const url = getImageURL(imgData, radius * 2, radius * 2);
const nodeElements = document.querySelectorAll(".node");
for (let i = 0; i < triangleNodes.length; i++) {
const el = nodeElements[i];
const node = triangleNodes[i];
const x = viewWidth / 2 - radius + node.x * radius;
const y = viewHeight / 2 - radius + node.y * radius;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
el.style.backgroundImage = `url(${url})`;
el.style.transform = `matrix(${calcMatrix(node)
.map((n) => n.toFixed(2))
.join(",")},0,0)`;
}
}, 1000 / 60);
function calcMatrix(node) {
let a = 1;
let b = 0;
let c = 0;
let d = 1;
for (const angle of node.path) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const a2 = -Math.pow(cos, 2) + Math.pow(sin, 2);
const b2 = -2 * cos * sin;
const c2 = -2 * cos * sin;
const d2 = Math.pow(cos, 2) - Math.pow(sin, 2);
const a3 = a2 * a + c2 * b;
const b3 = b2 * a + d2 * b;
const c3 = a2 * c + c2 * d;
const d3 = b2 * c + d2 * d;
a = a3;
b = b3;
c = c3;
d = d3;
}
return [a, b, c, d];
}
function getImageURL(imgData, width, height) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
ctx.putImageData(imgData, 0, 0);
return canvas.toDataURL(); //image URL
}
function createTriangleNodes(generation) {
const nodes = new Map();
const firstNode = { path: [], x: 0, y: 0 }; // r = 1 で正規化
function nodeId(node) {
return (
Math.round(node.x * 100) / 100 +
"," +
Math.round(node.y * 100) / 100
);
}
nodes.set(nodeId(firstNode), firstNode);
let prev = [firstNode];
for (let i = 0; i < generation; i++) {
const next = [];
const directions =
i % 2 === 0
? [(Math.PI / 6) * 3, (Math.PI / 6) * 7, (Math.PI / 6) * 11]
: [(Math.PI / 6) * 1, (Math.PI / 6) * 5, (Math.PI / 6) * 9];
for (const node of prev) {
for (const direction of directions) {
const newNode = {
path: [...node.path, direction],
x: node.x + Math.cos(direction),
y: node.y + Math.sin(direction),
};
const id = nodeId(newNode);
if (!nodes.has(id)) {
nodes.set(id, newNode);
next.push(newNode);
}
}
}
prev = next;
}
return [...nodes.values()];
}
const triangleNodes = createTriangleNodes(generation);
const viewElement = document.getElementById("view");
viewElement.style.position = "relative";
viewElement.style.width = `${viewWidth}px`;
viewElement.style.height = `${viewHeight}px`;
viewElement.style.border = "1px solid #000";
viewElement.style.overflow = "hidden";
for (const nodes of triangleNodes) {
const el = document.createElement("div");
el.style.position = "absolute";
el.className = "node";
el.style.width = `${radius * 2}px`;
el.style.height = `${radius * 2}px`;
el.style.backgroundPosition = "center";
el.style.backgroundRepeat = "no-repeat";
el.style.clipPath = `polygon(${radius}px ${0}px, ${
radius + radius * Math.cos(Math.PI / 6)
}px ${radius + radius * Math.sin(Math.PI / 6)}px, ${
radius + radius * Math.cos((Math.PI / 6) * 5)
}px ${radius + radius * Math.sin((Math.PI / 6) * 5)}px)`;
viewElement.appendChild(el);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment