Skip to content

Commit

Permalink
implement enableTimeControl and matter.js demo
Browse files Browse the repository at this point in the history
  • Loading branch information
matthen committed Oct 27, 2023
1 parent 45ad9ba commit fd60f77
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 8 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@types/react": "^18.2.30",
"@types/react-dom": "^18.2.14",
"chroma-js": "^2.4.2",
"matter-js": "^0.19.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
Expand Down Expand Up @@ -47,6 +48,7 @@
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/chroma-js": "^2.4.2",
"@types/matter-js": "^0.19.2",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"prettier-plugin-tailwindcss": "^0.5.6",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#282a36" />
<script type="text/javascript" src="CCapture.all.min.js"></script>
<script type="text/javascript" src="smoothstep/CCapture.all.min.js"></script>
<title>smoothstep</title>
</head>
<body>
Expand Down
150 changes: 150 additions & 0 deletions src/animations/ballspin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import Matter from 'matter-js';

import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation';
import Graphics from 'lib/graphics';
import Utils from 'lib/utils';

const Ballspin = () => {
const duration = 24;
const canvasWidth = 1024;
const canvasHeight = 1024;
const bgColor = '#020115';

const parameters: Parameter[] = [];

const makeDrawFn: MakeDrawFn = (canvas) => {
const ctx = canvas.getContext('2d')!;
const plotRange = 512;

const engine = Matter.Engine.create();
engine.gravity = { x: 0, y: -1, scale: 300.0 };
const ballInitParams = {
x: plotRange / 2,
y: 300,
vx: 1200,
vy: 1000,
angle: 0,
angularVelocity: 0.0,
};
const ballSpin = Matter.Bodies.circle(ballInitParams.x, ballInitParams.y, 20, {
restitution: 0.99,
friction: 0.2,
density: 0.1,
frictionStatic: 10.0,
collisionFilter: { mask: 2, category: 1 },
});
const ballNoSpin = Matter.Bodies.circle(ballInitParams.x, ballInitParams.y, 20, {
restitution: 0.99,
friction: 0.2,
density: 0.1,
frictionStatic: 10.0,
// inertia: Infinity,
collisionFilter: { mask: 2, category: 1 },
});
const numPieces = 40;
const radius = (0.9 * plotRange) / 2;
const groundPieces = Utils.range(0, Math.PI + 1e-6, Math.PI / (numPieces - 1)).map((th) =>
Matter.Bodies.rectangle(
plotRange / 2 - radius * Math.cos(th),
plotRange / 2 - radius * Math.sin(th),
6,
(2 * (Math.PI * radius)) / numPieces,
{
isStatic: true,
angle: th,
collisionFilter: { category: 2, mask: 1 },
},
),
);

Matter.World.add(engine.world, [ballSpin, ballNoSpin, ...groundPieces]);
let lastT = 0.0;

const ballGraphics = (ball: Matter.Body) => [
Graphics.Translate({ offset: [ball.position.x, ball.position.y] }),
Graphics.Rotate({ angle: -ball.angle, center: [0, 0] }),
Graphics.Disk({
center: [0, 0],
radius: ball.circleRadius!,
fill: true,
edge: false,
}),
Graphics.Line({
pts: [
[-ball.circleRadius!, 0],
[ball.circleRadius!, 0],
],
}),
];

const initBall = (ball: Matter.Body, nudge: number) => {
Matter.Body.setPosition(ball, { x: ballInitParams.x, y: ballInitParams.y });
Matter.Body.setVelocity(ball, { x: ballInitParams.vx + nudge, y: ballInitParams.vy });
Matter.Body.setAngle(ball, ballInitParams.angle);
Matter.Body.setAngularVelocity(ball, ballInitParams.angularVelocity);
};

let trace1: number[][] = [];
let trace2: number[][] = [];

const drawFn: DrawFn = ({ t }: DrawArgs) => {
if (t == 0.0) {
initBall(ballSpin, 0);
initBall(ballNoSpin, 0.1);
trace1 = [];
trace2 = [];
}
const deltaT = t - lastT;
lastT = t;
if (deltaT > 0 && deltaT < 0.1) {
Matter.Engine.update(engine, deltaT / 2);
Matter.Engine.update(engine, deltaT / 2);
trace1.push([ballNoSpin.position.x, ballNoSpin.position.y]);
trace2.push([ballSpin.position.x, ballSpin.position.y]);
}

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);

Graphics.draw(
[
Graphics.AbsoluteLineWidth(4),
[Graphics.Set({ strokeStyle: '#985e00ff' }), Graphics.Line({ pts: trace1 })],
[Graphics.Set({ strokeStyle: '#5290a5ff' }), Graphics.Line({ pts: trace2 })],
[Graphics.Set({ fillStyle: '#7dddfca0', strokeStyle: bgColor }), ballGraphics(ballSpin)],
[Graphics.Set({ fillStyle: '#ff9d00b5', strokeStyle: bgColor }), ballGraphics(ballNoSpin)],
// ground
Graphics.Set({ fillStyle: '#ff000062', strokeStyle: '#ffffffff', lineWidth: 6 }),
Graphics.Disk({ center: [plotRange / 2, plotRange / 2], radius, fill: false, edge: true }),
// groundPieces.map((ground) =>
// Graphics.Polygon({ pts: ground.vertices.map((pt) => [pt.x, pt.y]), edge: false, fill: true }),
// ),
],

{
xmin: 0,
xmax: plotRange,
ymin: 0,
ymax: plotRange,
},
ctx,
);
};

return drawFn;
};

return (
<Animation
duration={duration}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
makeDrawFn={makeDrawFn}
parameters={parameters}
enableTimeControl={false}
/>
);
};

export default Ballspin;
27 changes: 25 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import Ballspin from 'animations/ballspin';
import React, { ReactNode } from 'react';
import ReactDOM from 'react-dom/client';
import { FaArrowLeft } from 'react-icons/fa';
import { Link, RouterProvider, createHashRouter } from 'react-router-dom';

import Hypocycloids from './animations/hypocycloids';
Expand All @@ -11,6 +13,10 @@ const animations = [
name: 'hypocycloids',
component: Hypocycloids,
},
{
name: 'ballspin',
component: Ballspin,
},
];

const AnimationList = () => {
Expand All @@ -30,6 +36,19 @@ const AnimationList = () => {
);
};

const ViewAnimation = ({ children }: { children: ReactNode }) => {
return (
<div>
<p className="mb-2">
<Link to="/" className="text-sm text-neutral-400 hover:text-white">
<FaArrowLeft className="mb-1 mr-1 inline " /> all animations
</Link>
</p>
{children}
</div>
);
};

const router = createHashRouter([
{
path: '/',
Expand All @@ -38,7 +57,11 @@ const router = createHashRouter([
...animations.map((animation) => {
return {
path: `/${animation.name}`,
element: <animation.component />,
element: (
<ViewAnimation>
<animation.component />
</ViewAnimation>
),
};
}),
]);
Expand Down
24 changes: 20 additions & 4 deletions src/lib/Animation.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { IconContext } from 'react-icons';
import { FaPause, FaPlay } from 'react-icons/fa';
import { FaPause, FaPlay, FaStepBackward } from 'react-icons/fa';

export type DrawArgs = Record<string, number>;
export type DrawArgs = Record<string, number> & { t: number };
export type DrawFn = (args: DrawArgs) => void;
export type MakeDrawFn = (canvas: HTMLCanvasElement) => DrawFn;
export interface Parameter {
Expand All @@ -22,9 +22,11 @@ interface AnimationOptions {
pixelRatio?: number;
makeDrawFn: MakeDrawFn;
parameters: Parameter[];
enableTimeControl?: boolean;
}

export const Animation = (props: AnimationOptions) => {
const enableTimeControl = props.enableTimeControl === undefined ? true : props.enableTimeControl;
const [drawFn, setDrawFn] = useState<DrawFn | null>(null);
const [controlMode, setControlMode] = useState('user' as 'playing' | 'user' | 'recording');
const computeParamValues = (t: number): Record<string, number> =>
Expand Down Expand Up @@ -149,6 +151,13 @@ export const Animation = (props: AnimationOptions) => {
}
}, [controlMode]);

const onClickReset = useCallback(() => {
if (controlMode == 'playing') {
setControlMode('user');
}
setDrawArgsUI((old) => ({ ...old, t: 0.0, ...computeParamValues(0) }));
}, [controlMode]);

const onClickRecord = useCallback(() => {
setControlMode('recording');
}, []);
Expand Down Expand Up @@ -212,6 +221,13 @@ export const Animation = (props: AnimationOptions) => {
</div>
<div className="grid grid-cols-8 gap-2">
<div className="col-span-1 pr-2 text-right">
<button
className="mr-2 text-light-200 hover:text-light disabled:text-neutral-400 disabled:hover:text-neutral-400"
onClick={() => onClickReset()}
disabled={controlMode == 'recording'}
>
<FaStepBackward />
</button>
<button
className="text-light-200 hover:text-light disabled:text-neutral-400 disabled:hover:text-neutral-400"
onClick={() => onClickPlayPause()}
Expand All @@ -227,7 +243,7 @@ export const Animation = (props: AnimationOptions) => {
max={props.duration}
value={drawArgsUI.t}
step={0.01}
disabled={controlMode != 'user'}
disabled={controlMode != 'user' || !enableTimeControl}
className="h-2 w-full appearance-none rounded-lg bg-dark accent-pink"
onChange={(e) =>
setDrawArgsUI((old) => {
Expand All @@ -247,7 +263,7 @@ export const Animation = (props: AnimationOptions) => {
max={props.duration}
value={Math.round(drawArgsUI.t * 100) / 100}
step={0.01}
disabled={controlMode != 'user'}
disabled={controlMode != 'user' || !enableTimeControl}
className="ml-2 w-20 appearance-none rounded bg-dark px-2 py-1"
onChange={(e) =>
setDrawArgsUI((old) => {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ namespace Graphics {
radius,
fill,
edge,
startAngle = 0,
endAngle = 2 * Math.PI,
}: {
center: number[];
radius: number;
fill: boolean;
edge: boolean;
startAngle?: number;
endAngle?: number;
}): DrawCommand => {
return (ctx) => {
if (!fill && !edge) {
Expand All @@ -62,7 +66,7 @@ namespace Graphics {
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
}
ctx.beginPath();
ctx.arc(center[0], -center[1], radius, 0, 2 * Math.PI);
ctx.arc(center[0], -center[1], radius, startAngle, endAngle);
ctx.fill();
if (edge) {
ctx.stroke();
Expand Down

0 comments on commit fd60f77

Please sign in to comment.