diff --git a/.eleventy.js b/.eleventy.js
deleted file mode 100644
index 4c9d6f1..0000000
--- a/.eleventy.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function (eleventyConfig) {
- eleventyConfig.addPassthroughCopy('src/assets/css')
- eleventyConfig.addPassthroughCopy("src/assets/favicon.ico");
- eleventyConfig.addPassthroughCopy("src/objects");
- eleventyConfig.setServerPassthroughCopyBehavior("passthrough");
-
- return {
- dir: {
- input: "src",
- output: "dist"
- }
- }
-};
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..0a02ffe
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+ Orion Reed
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 1d4c110..9cdf5ed 100644
--- a/package.json
+++ b/package.json
@@ -4,13 +4,29 @@
"description": "Orion Reed's personal website",
"type": "module",
"scripts": {
- "dev": "npx @11ty/eleventy --serve",
- "build": "npx @11ty/eleventy"
+ "dev": "vite",
+ "build": "tsc && vite build --base=./",
+ "preview": "tsc && vite build --base=./ && vite preview"
},
"keywords": [],
"author": "Orion Reed",
"license": "ISC",
+ "dependencies": {
+ "@dimforge/rapier2d": "^0.11.2",
+ "@tldraw/tldraw": "2.0.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.3"
+ },
"devDependencies": {
- "@11ty/eleventy": "3.0.0-alpha.5"
+ "@biomejs/biome": "1.4.1",
+ "@types/gh-pages": "^6",
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@vitejs/plugin-react": "^4.0.3",
+ "typescript": "^5.0.2",
+ "vite": "^4.4.5",
+ "vite-plugin-top-level-await": "^1.3.1",
+ "vite-plugin-wasm": "^3.2.2"
}
}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..fa69277
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,53 @@
+import "@tldraw/tldraw/tldraw.css";
+import "@/css/style.css"
+import React, { useEffect, useState } from "react";
+import ReactDOM from "react-dom/client";
+import { Default } from "@/components/Default";
+import { Canvas } from "@/components/Canvas";
+import { Toggle } from "@/components/Toggle";
+import { useCanvas } from "@/hooks/useCanvas"
+import { createShapes } from "@/utils";
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import { Contact } from "@/components/Contact";
+
+ReactDOM.createRoot(document.getElementById("root")!).render( );
+
+function App() {
+
+
+ return (
+
+
+
+ } />
+ } />
+
+
+
+ );
+};
+
+function Home() {
+ const { isCanvasEnabled, elementsInfo } = useCanvas();
+ const shapes = createShapes(elementsInfo)
+ const [isEditorMounted, setIsEditorMounted] = useState(false);
+
+ useEffect(() => {
+ const handleEditorDidMount = () => {
+ setIsEditorMounted(true);
+ };
+
+ window.addEventListener('editorDidMountEvent', handleEditorDidMount);
+
+ return () => {
+ window.removeEventListener('editorDidMountEvent', handleEditorDidMount);
+ };
+ }, []);
+
+ return (
+ <>
+
+ { }
+
+ {isCanvasEnabled && elementsInfo.length > 0 ? : null}>)
+}
\ No newline at end of file
diff --git a/src/_includes/layout.html b/src/_includes/layout.html
deleted file mode 100644
index a10ff27..0000000
--- a/src/_includes/layout.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- Orion Reed
-
-
-
-
-
-
-
-
-
-
-
-
- {{ content }}
-
-
-
\ No newline at end of file
diff --git a/src/assets/css/style.css b/src/assets/css/style.css
deleted file mode 100644
index 8926a69..0000000
--- a/src/assets/css/style.css
+++ /dev/null
@@ -1,64 +0,0 @@
-@import url("reset.css");
-
-body {
- max-width: 60em;
- margin: 0 auto;
- padding-left: 4em;
- padding-right: 4em;
- padding-top: 3em;
- padding-bottom: 3em;
- font-family: "Recursive";
- font-variation-settings: "MONO" 1;
- font-variation-settings: "CASL" 1;
- color: #24292e;
-}
-
-h1 {
- font-size: 1.5rem;
- margin-top: 1em;
-}
-
-header {
- margin-bottom: 2em;
- font-size: 1.5em;
- font-variation-settings: "MONO" 1;
- font-variation-settings: "CASL" 1;
-}
-
-i {
- font-variation-settings: "slnt" -15;
-}
-
-p {
- font-family: Recursive;
- margin-top: 0;
- margin-bottom: 0.6em;
- font-size: 1.1em;
- font-variation-settings: "wght" 350;
-}
-.dinkus {
- display: block;
- text-align: center;
- font-size: 1em;
- margin-top: 2em;
- margin-bottom: 0em;
-}
-ul {
- padding-left: 0;
- list-style: decimal-leading-zero;
- & li::marker {
- color: rgba(0, 0, 0, 0.322);
- }
-}
-
-@media (max-width: 600px) {
- body {
- padding: 2em;
- }
- header {
- margin-bottom: 1em;
- }
- ul {
- list-style-position: inside;
- }
-}
diff --git a/src/card/contact.md b/src/card/contact.md
deleted file mode 100644
index bbe37b0..0000000
--- a/src/card/contact.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-layout: layout.html
----
-
-# Contact
-
-Twitter: [@OrionReedOne](https://twitter.com/OrionReedOne)
-Mastodon: [orion@hci.social](https://hci.social/@orion)
-Email: [me@orionreed.com](mailto:me@orionreed.com)
-GitHub: [OrionReed](https://github.com/orionreed)
\ No newline at end of file
diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx
new file mode 100644
index 0000000..67492b9
--- /dev/null
+++ b/src/components/Canvas.tsx
@@ -0,0 +1,40 @@
+import { Tldraw, TLShape, TLUiComponents } from "@tldraw/tldraw";
+import { SimController } from "@/physics/PhysicsControls";
+import { HTMLShapeUtil } from "@/shapes/HTMLShapeUtil";
+
+const components: TLUiComponents = {
+ HelpMenu: null,
+ StylePanel: null,
+ PageMenu: null,
+ NavigationPanel: null,
+ DebugMenu: null,
+ ContextMenu: null,
+ ActionsMenu: null,
+ QuickActions: null,
+ MainMenu: null,
+ MenuPanel: null,
+ // ZoomMenu: null,
+ // Minimap: null,
+ // Toolbar: null,
+ // KeyboardShortcutsDialog: null,
+ // HelperButtons: null,
+ // SharePanel: null,
+ // TopPanel: null,
+}
+
+export function Canvas({ shapes }: { shapes: TLShape[]; }) {
+
+ return (
+
+ {
+ window.dispatchEvent(new CustomEvent('editorDidMountEvent'));
+ }}
+ >
+
+
+
+ );
+}
diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx
new file mode 100644
index 0000000..f592cd2
--- /dev/null
+++ b/src/components/Contact.tsx
@@ -0,0 +1,17 @@
+export function Contact() {
+ return (
+
+
+ Contact
+ Twitter: @OrionReedOne
+ Mastodon: orion@hci.social
+ Email: me@orionreed.com
+ GitHub: OrionReed
+
+
+ );
+}
diff --git a/src/components/Default.tsx b/src/components/Default.tsx
new file mode 100644
index 0000000..3fe1071
--- /dev/null
+++ b/src/components/Default.tsx
@@ -0,0 +1,57 @@
+export function Default() {
+ return (
+
+
+ Hello! 👋
+
+ My research investigates the intersection of computing, human-system
+ interfaces, and emancipatory politics. I am interested in the
+ potential of computing as a medium for thought, as a tool for
+ collective action, and as a means of emancipation.
+
+
+
+ My current focus is basic research into the nature of digital
+ organisation, developing theoretical toolkits to improve shared
+ infrastructure, and applying this research to the design of new
+ systems and protocols which support the self-organisation of knowledge
+ and computational artifacts.
+
+
+ My work
+
+ Alongside my independent work I am a researcher at Block Science building{' '}
+ knowledge organisation infrastructure and at ECSA working on{' '}
+ computational media . I am also part of the nascent Liberatory Computing {' '}
+ collective and a co-organiser of the OCWG .
+
+
+ Get in touch
+
+ I am on Twitter as @OrionReedOne and on
+ Mastodon as @orion@hci.social . The best way to reach me is
+ through Twitter or my email, me@orionreed.com
+
+
+ ***
+
+ Talks
+
+ Writing
+
+
+ );
+}
diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx
new file mode 100644
index 0000000..8b27dbd
--- /dev/null
+++ b/src/components/Toggle.tsx
@@ -0,0 +1,12 @@
+export function Toggle() {
+ return (
+ <>
+ window.dispatchEvent(new CustomEvent('toggleCanvasEvent'))}>
+
+
+ window.dispatchEvent(new CustomEvent('togglePhysicsEvent'))}>
+
+
+ >
+ );
+}
diff --git a/src/assets/css/reset.css b/src/css/reset.css
similarity index 100%
rename from src/assets/css/reset.css
rename to src/css/reset.css
diff --git a/src/css/style.css b/src/css/style.css
new file mode 100644
index 0000000..8dc7347
--- /dev/null
+++ b/src/css/style.css
@@ -0,0 +1,168 @@
+@import url("reset.css");
+
+html,
+body {
+ padding: 0;
+ margin: 0;
+ touch-action: none;
+ min-height: 100vh;
+ min-height: -webkit-fill-available;
+ height: 100%;
+ /* font-family: "Inter", sans-serif; */
+}
+
+.tldraw__editor {
+ overscroll-behavior: none;
+ position: fixed;
+ inset: 0px;
+ overflow: hidden;
+}
+
+.tl-background {
+ background-color: transparent;
+}
+
+.tlui-debug-panel {
+ display: none;
+}
+
+/* Non-tldraw stuff */
+
+main {
+ max-width: 60em;
+ margin: 0 auto;
+ padding-left: 4em;
+ padding-right: 4em;
+ padding-top: 3em;
+ padding-bottom: 3em;
+ font-family: "Recursive";
+ font-variation-settings: "MONO" 1;
+ font-variation-settings: "CASL" 1;
+ color: #24292e;
+}
+
+h1 {
+ font-size: 1.5rem;
+ margin-top: 1em;
+}
+
+header {
+ margin-bottom: 2em;
+ font-size: 1.5rem;
+ font-variation-settings: "MONO" 1;
+ font-variation-settings: "CASL" 1;
+}
+
+i {
+ font-variation-settings: "slnt" -15;
+}
+
+p {
+ font-family: Recursive;
+ margin-top: 0;
+ margin-bottom: 0.6em;
+ font-size: 1.1em;
+ font-variation-settings: "wght" 350;
+}
+
+a {
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+p a {
+ text-decoration: underline;
+}
+
+.dinkus {
+ display: block;
+ text-align: center;
+ font-size: 1.1rem;
+ margin-top: 2em;
+ margin-bottom: 0em;
+}
+
+ul {
+ padding-left: 0;
+ font-size: 1rem;
+ list-style: decimal-leading-zero;
+ & li::marker {
+ color: rgba(0, 0, 0, 0.322);
+ }
+}
+
+#toggle-physics,
+#toggle-canvas {
+ position: absolute;
+ z-index: 99999;
+ right: 10px;
+ width: 50px;
+ height: 50px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ opacity: 0.25;
+ &:hover {
+ opacity: 1;
+ }
+ & img {
+ width: 100%;
+ height: 100%;
+ }
+}
+#toggle-canvas {
+ top: 10px;
+ & img {
+ width: 50%;
+ height: 50%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+#toggle-physics {
+ top: 60px;
+}
+
+@media (max-width: 600px) {
+ main {
+ padding: 2em;
+ }
+ header {
+ margin-bottom: 1em;
+ }
+ ul {
+ list-style-position: inside;
+ }
+}
+
+.tl-html-layer {
+ font-family: "Recursive";
+ font-variation-settings: "MONO" 1;
+ font-variation-settings: "CASL" 1;
+ & h1,
+ p,
+ span,
+ header,
+ ul {
+ margin: 0;
+ }
+ & header {
+ font-size: 1.5rem;
+ }
+ & p {
+ font-size: 1.1rem;
+ }
+}
+
+.transparent {
+ opacity: 0 !important;
+ transition: opacity 0.25s ease-in-out;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/src/hooks/useCanvas.ts b/src/hooks/useCanvas.ts
new file mode 100644
index 0000000..6d01834
--- /dev/null
+++ b/src/hooks/useCanvas.ts
@@ -0,0 +1,105 @@
+import { useState, useEffect } from 'react';
+
+interface ElementInfo {
+ tagName: string;
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ html: string;
+}
+
+export function useCanvas() {
+ const [isCanvasEnabled, setIsCanvasEnabled] = useState(false);
+ const [elementsInfo, setElementsInfo] = useState([]);
+
+ useEffect(() => {
+ const toggleCanvas = async () => {
+ if (!isCanvasEnabled) {
+ const info = await gatherElementsInfo();
+ setElementsInfo(info);
+ setIsCanvasEnabled(true);
+ document.getElementById('toggle-physics')?.classList.remove('hidden');
+ } else {
+ setElementsInfo([]);
+ setIsCanvasEnabled(false);
+ document.getElementById('toggle-physics')?.classList.add('hidden');
+ }
+ };
+ // const enableCanvas = async () => {
+ // const info = await gatherElementsInfo();
+ // setElementsInfo(info);
+ // setIsCanvasEnabled(true);
+ // };
+
+ window.addEventListener('toggleCanvasEvent', toggleCanvas);
+ // window.addEventListener('togglePhysicsEvent', enableCanvas);
+
+ return () => {
+ window.removeEventListener('toggleCanvasEvent', toggleCanvas);
+ // window.removeEventListener('togglePhysicsEvent', enableCanvas);
+ };
+ }, [isCanvasEnabled]);
+
+ return { isCanvasEnabled, elementsInfo };
+}
+
+async function gatherElementsInfo() {
+ const rootElement = document.getElementsByTagName('main')[0];
+ const info: any[] = [];
+ if (rootElement) {
+ for (const child of rootElement.children) {
+ if (['BUTTON'].includes(child.tagName)) continue;
+ const rect = child.getBoundingClientRect();
+ let w = rect.width;
+ if (!['P', 'UL'].includes(child.tagName)) {
+ w = measureElementTextWidth(child as HTMLElement);
+ }
+ // Check if the element is centered
+ const computedStyle = window.getComputedStyle(child);
+ let x = rect.left; // Default x position
+ if (computedStyle.display === 'block' && computedStyle.textAlign === 'center') {
+ // Adjust x position for centered elements
+ const parentWidth = child.parentElement ? child.parentElement.getBoundingClientRect().width : 0;
+ x = (parentWidth - w) / 2 + window.scrollX + (child.parentElement ? child.parentElement.getBoundingClientRect().left : 0);
+ }
+
+ info.push({
+ tagName: child.tagName,
+ x: x,
+ y: rect.top,
+ w: w,
+ h: rect.height,
+ html: child.outerHTML
+ });
+ };
+ }
+ return info;
+}
+
+function measureElementTextWidth(element: HTMLElement) {
+ // Create a temporary span element
+ const tempElement = document.createElement('span');
+ // Get the text content from the passed element
+ tempElement.textContent = element.textContent || element.innerText;
+ // Get the computed style of the passed element
+ const computedStyle = window.getComputedStyle(element);
+ // Apply relevant styles to the temporary element
+ tempElement.style.font = computedStyle.font;
+ tempElement.style.fontWeight = computedStyle.fontWeight;
+ tempElement.style.fontSize = computedStyle.fontSize;
+ tempElement.style.fontFamily = computedStyle.fontFamily;
+ tempElement.style.letterSpacing = computedStyle.letterSpacing;
+ // Ensure the temporary element is not visible in the viewport
+ tempElement.style.position = 'absolute';
+ tempElement.style.visibility = 'hidden';
+ tempElement.style.whiteSpace = 'nowrap'; // Prevent text from wrapping
+ // Append to the body to make measurements possible
+ document.body.appendChild(tempElement);
+ // Measure the width
+ const width = tempElement.getBoundingClientRect().width;
+ // Remove the temporary element from the document
+ document.body.removeChild(tempElement);
+ // Return the measured width
+ return width === 0 ? 10 : width;
+}
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
deleted file mode 100644
index 37ab904..0000000
--- a/src/index.html
+++ /dev/null
@@ -1,53 +0,0 @@
----
-layout: layout.html
----
-
-Hello! 👋
-
- My research investigates the intersection of computing, human-system
- interfaces, and emancipatory politics. I am interested in the
- potential of computing as a medium for thought, as a tool for
- collective action, and as a means of emancipation.
-
-
-
- My current focus is basic research into the nature of digital
- organisation, developing theoretical toolkits to improve shared
- infrastructure, and applying this research to the design of new
- systems and protocols which support the self-organisation of knowledge
- and computational artifacts.
-
-
-My work
-
- Alongside my independent work I am a researcher at
- Block Science building
- knowledge organisation infrastructure and at
- ECSA working on
- computational media . I am also part of the nascent Liberatory Computing
- collective and a co-organiser of the OCWG .
-
-
-Get in touch
-
- I am on Twitter as @OrionReedOne and on
- Mastodon as @orion@hci.social . The best way to reach me is
- through Twitter or my email, me@orionreed.com
-
-
-***
-
-Talks
-
-Writing
-
diff --git a/src/physics/PhysicsControls.tsx b/src/physics/PhysicsControls.tsx
new file mode 100644
index 0000000..590cd76
--- /dev/null
+++ b/src/physics/PhysicsControls.tsx
@@ -0,0 +1,48 @@
+import { TLUnknownShape, useEditor } from "@tldraw/tldraw";
+import { useEffect, useState } from "react";
+import { usePhysicsSimulation } from "./simulation";
+
+export const SimController = ({ shapes }: { shapes: TLUnknownShape[] }) => {
+ const editor = useEditor();
+ const [isPhysicsActive, setIsPhysicsActive] = useState(false);
+ const { addShapes, destroy } = usePhysicsSimulation(editor);
+
+ useEffect(() => {
+ editor.createShapes(shapes)
+ return () => { editor.deleteShapes(editor.getCurrentPageShapes()) }
+ }, []);
+
+ useEffect(() => {
+ const togglePhysics = () => {
+ setIsPhysicsActive((currentIsPhysicsActive) => {
+ if (currentIsPhysicsActive) {
+ console.log('destroy');
+ destroy();
+ return false;
+ } else {
+ console.log('activate');
+ return true;
+ }
+ });
+ };
+
+ // Listen for the togglePhysicsEvent to enable/disable physics simulation
+ window.addEventListener('togglePhysicsEvent', togglePhysics);
+
+ return () => {
+ window.removeEventListener('togglePhysicsEvent', togglePhysics);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (isPhysicsActive) {
+ console.log('adding shapes');
+
+ addShapes(editor.getCurrentPageShapes()); // Activate physics simulation
+ } else {
+ destroy(); // Deactivate physics simulation
+ }
+ }, [isPhysicsActive, addShapes, shapes]);
+
+ return (<>>);
+};
diff --git a/src/physics/config.ts b/src/physics/config.ts
new file mode 100644
index 0000000..1876589
--- /dev/null
+++ b/src/physics/config.ts
@@ -0,0 +1,36 @@
+export const GRAVITY = { x: 0.0, y: 98 };
+export const DEFAULT_RESTITUTION = 0;
+export const DEFAULT_FRICTION = 0.1;
+
+export function isRigidbody(color: string) {
+ return !color || color === "black" ? false : true;
+}
+export function getGravityFromColor(color: string) {
+ return color === 'grey' ? 0 : 1
+}
+export function getRestitutionFromColor(color: string) {
+ return color === "orange" ? 0.9 : 0;
+}
+export function getFrictionFromColor(color: string) {
+ return color === "blue" ? 0.1 : 0.8;
+}
+export const MATERIAL = {
+ defaultRestitution: 0,
+ defaultFriction: 0.1,
+};
+export const CHARACTER = {
+ up: { x: 0.0, y: -1.0 },
+ additionalMass: 20,
+ maxSlopeClimbAngle: 1,
+ slideEnabled: true,
+ minSlopeSlideAngle: 0.9,
+ applyImpulsesToDynamicBodies: true,
+ autostepHeight: 5,
+ autostepMaxClimbAngle: 1,
+ snapToGroundDistance: 3,
+ maxMoveSpeedX: 100,
+ moveAcceleration: 600,
+ moveDeceleration: 500,
+ jumpVelocity: 300,
+ gravityMultiplier: 10,
+};
diff --git a/src/physics/math.ts b/src/physics/math.ts
new file mode 100644
index 0000000..bb44fd0
--- /dev/null
+++ b/src/physics/math.ts
@@ -0,0 +1,95 @@
+import { Geometry2d, Vec, VecLike } from "@tldraw/tldraw";
+
+type ShapeTransform = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ rotation: number;
+ parent?: Geometry2d;
+}
+
+// Define rotatePoint as a standalone function
+const rotatePoint = (cx: number, cy: number, x: number, y: number, angle: number) => {
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ return {
+ x: cos * (x - cx) - sin * (y - cy) + cx,
+ y: sin * (x - cx) + cos * (y - cy) + cy,
+ };
+}
+
+export const cornerToCenter = ({
+ x,
+ y,
+ width,
+ height,
+ rotation,
+ parent
+}: ShapeTransform): { x: number; y: number } => {
+ const centerX = x + width / 2;
+ const centerY = y + height / 2;
+ const rotatedCenter = rotatePoint(x, y, centerX, centerY, rotation);
+
+ if (parent) {
+ rotatedCenter.x -= parent.center.x;
+ rotatedCenter.y -= parent.center.y;
+ }
+
+ return rotatedCenter;
+}
+
+export const centerToCorner = ({
+ x,
+ y,
+ width,
+ height,
+ rotation,
+}: ShapeTransform): { x: number; y: number } => {
+
+ const cornerX = x - width / 2;
+ const cornerY = y - height / 2;
+
+ return rotatePoint(x, y, cornerX, cornerY, rotation);
+}
+
+export const getDisplacement = (
+ velocity: VecLike,
+ acceleration: VecLike,
+ timeStep: number,
+ speedLimitX: number,
+ decelerationX: number,
+): VecLike => {
+ let newVelocityX =
+ acceleration.x === 0 && velocity.x !== 0
+ ? Math.max(Math.abs(velocity.x) - decelerationX * timeStep, 0) *
+ Math.sign(velocity.x)
+ : velocity.x + acceleration.x * timeStep;
+
+ newVelocityX =
+ Math.min(Math.abs(newVelocityX), speedLimitX) * Math.sign(newVelocityX);
+
+ const averageVelocityX = (velocity.x + newVelocityX) / 2;
+ const x = averageVelocityX * timeStep;
+ const y =
+ velocity.y * timeStep + 0.5 * acceleration.y * timeStep ** 2;
+
+ return { x, y }
+}
+
+export const convertVerticesToFloat32Array = (
+ vertices: Vec[],
+ width: number,
+ height: number,
+) => {
+ const vec2Array = new Float32Array(vertices.length * 2);
+ const hX = width / 2;
+ const hY = height / 2;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vec2Array[i * 2] = vertices[i].x - hX;
+ vec2Array[i * 2 + 1] = vertices[i].y - hY;
+ }
+
+ return vec2Array;
+}
diff --git a/src/physics/simulation.ts b/src/physics/simulation.ts
new file mode 100644
index 0000000..31c318e
--- /dev/null
+++ b/src/physics/simulation.ts
@@ -0,0 +1,398 @@
+import RAPIER from "@dimforge/rapier2d";
+import { CHARACTER, GRAVITY, MATERIAL, getFrictionFromColor, getGravityFromColor, getRestitutionFromColor, isRigidbody } from "./config";
+import { Editor, Geometry2d, TLDrawShape, TLGeoShape, TLGroupShape, TLShape, TLShapeId, VecLike } from "@tldraw/tldraw";
+import { useEffect, useRef } from "react";
+import { centerToCorner, convertVerticesToFloat32Array, cornerToCenter, getDisplacement } from "./math";
+
+type BodyWithShapeData = RAPIER.RigidBody & {
+ userData: { id: TLShapeId; type: TLShape["type"]; w: number; h: number };
+};
+type RigidbodyLookup = { [key: TLShapeId]: RAPIER.RigidBody };
+
+export class PhysicsWorld {
+ private editor: Editor;
+ private world: RAPIER.World;
+ private rigidbodyLookup: RigidbodyLookup;
+ private animFrame = -1; // Store the animation frame id
+ private character: {
+ rigidbody: RAPIER.RigidBody | null;
+ collider: RAPIER.Collider | null;
+ };
+ constructor(editor: Editor) {
+ this.editor = editor
+ this.world = new RAPIER.World(GRAVITY)
+ this.rigidbodyLookup = {}
+ this.character = { rigidbody: null, collider: null }
+ }
+
+ public start() {
+ this.world = new RAPIER.World(GRAVITY);
+
+ const simLoop = () => {
+ this.world.step();
+ this.updateCharacterControllers();
+ this.updateRigidbodies();
+ this.animFrame = requestAnimationFrame(simLoop);
+ };
+ simLoop();
+ return () => cancelAnimationFrame(this.animFrame);
+ };
+
+ public stop() {
+ if (this.animFrame !== -1) {
+ cancelAnimationFrame(this.animFrame);
+ this.animFrame = -1;
+ }
+ }
+
+ public addShapes(shapes: TLShape[]) {
+ console.log('adding shapesss');
+
+ for (const shape of shapes) {
+ console.log('shape');
+ if ('color' in shape.props && shape.props.color === "violet") {
+ this.createCharacter(shape as TLGeoShape);
+ continue;
+ }
+
+ switch (shape.type) {
+ case "html":
+ case "geo":
+ case "image":
+ case "video":
+ this.createShape(shape as TLGeoShape);
+ break;
+ case "draw":
+ this.createCompoundLine(shape as TLDrawShape);
+ break;
+ case "group":
+ this.createGroup(shape as TLGroupShape);
+ break;
+ // Add cases for any new shape types here
+ }
+ }
+ }
+
+ createShape(shape: TLGeoShape | TLDrawShape) {
+ if (!shape.meta.fixed) {
+ const rb = this.createRigidbody(shape, 1);
+ this.createCollider(shape, rb);
+ }
+ else {
+ this.createCollider(shape);
+ }
+ }
+
+ createCharacter(characterShape: TLGeoShape) {
+ const initialPosition = cornerToCenter({
+ x: characterShape.x,
+ y: characterShape.y,
+ width: characterShape.props.w,
+ height: characterShape.props.h,
+ rotation: characterShape.rotation,
+ });
+ const vertices = this.editor.getShapeGeometry(characterShape).vertices;
+ const vec2Array = convertVerticesToFloat32Array(
+ vertices,
+ characterShape.props.w,
+ characterShape.props.h,
+ );
+ const colliderDesc = RAPIER.ColliderDesc.convexHull(vec2Array);
+ if (!colliderDesc) {
+ console.error("Failed to create collider description.");
+ return;
+ }
+ const rigidBodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased()
+ .setTranslation(initialPosition.x, initialPosition.y)
+ .setAdditionalMass(CHARACTER.additionalMass);
+ const charRigidbody = this.world.createRigidBody(rigidBodyDesc);
+ const charCollider = this.world.createCollider(colliderDesc, charRigidbody);
+ const char = this.world.createCharacterController(0.1);
+ char.setUp(CHARACTER.up);
+ char.setMaxSlopeClimbAngle(CHARACTER.maxSlopeClimbAngle);
+ char.setSlideEnabled(CHARACTER.slideEnabled);
+ char.setMinSlopeSlideAngle(CHARACTER.minSlopeSlideAngle);
+ char.setApplyImpulsesToDynamicBodies(CHARACTER.applyImpulsesToDynamicBodies);
+ char.enableAutostep(
+ CHARACTER.autostepHeight,
+ CHARACTER.autostepMaxClimbAngle,
+ true,
+ );
+ char.enableSnapToGround(CHARACTER.snapToGroundDistance);
+ // Setup references so we can update character position in sim loop
+ this.character.rigidbody = charRigidbody;
+ this.character.collider = charCollider;
+ charRigidbody.userData = {
+ id: characterShape.id,
+ type: characterShape.type,
+ w: characterShape.props.w,
+ h: characterShape.props.h,
+ };
+ }
+
+ createGroup(group: TLGroupShape) {
+ // create rigidbody for group
+ const rigidbody = this.createRigidbody(group);
+ const rigidbodyGeometry = this.editor.getShapeGeometry(group);
+
+ this.editor.getSortedChildIdsForParent(group.id).forEach((childId) => {
+ // create collider for each
+ const child = this.editor.getShape(childId);
+ if (!child) return;
+ const isRb = "color" in child.props && isRigidbody(child?.props.color);
+ if (isRb) {
+ this.createCollider(child, rigidbody, rigidbodyGeometry);
+ } else {
+ this.createCollider(child);
+ }
+ });
+ }
+ createCompoundLine(drawShape: TLDrawShape) {
+ const rigidbody = this.createRigidbody(drawShape);
+ const drawnGeo = this.editor.getShapeGeometry(drawShape);
+ const verts = drawnGeo.vertices;
+ // const isRb =
+ // "color" in drawShape.props && isRigidbody(drawShape.props.color);
+ const isRb = true;
+ verts.forEach((point) => {
+ if (isRb) this.createColliderAtPoint(point, drawShape, rigidbody);
+ else this.createColliderAtPoint(point, drawShape);
+ });
+ }
+
+ updateRigidbodies() {
+ this.world.bodies.forEach((rb) => {
+ if (rb === this.character?.rigidbody) return;
+ if (!rb.userData) return;
+ const body = rb as BodyWithShapeData;
+ const position = body.translation();
+ const rotation = body.rotation();
+
+ const cornerPos = centerToCorner({
+ x: position.x,
+ y: position.y,
+ width: body.userData?.w,
+ height: body.userData?.h,
+ rotation: rotation,
+ });
+
+ this.editor.updateShape({
+ id: body.userData?.id,
+ type: body.userData?.type,
+ rotation: rotation,
+ x: cornerPos.x,
+ y: cornerPos.y,
+ });
+ });
+ }
+
+ updateCharacterControllers() {
+ const right = this.editor.inputs.keys.has("ArrowRight") ? 1 : 0;
+ const left = this.editor.inputs.keys.has("ArrowLeft") ? -1 : 0;
+ const acceleration: VecLike = {
+ x: (right + left) * CHARACTER.moveAcceleration,
+ y: CHARACTER.gravityMultiplier * GRAVITY.y,
+ }
+
+ this.world.characterControllers.forEach((char) => {
+ if (!this.character.rigidbody || !this.character.collider) return;
+ const charRigidbody = this.character.rigidbody as BodyWithShapeData;
+ const charCollider = this.character.collider;
+ const grounded = char.computedGrounded();
+ const isJumping = this.editor.inputs.keys.has("ArrowUp") && grounded;
+ const velocity: VecLike = {
+ x: charRigidbody.linvel().x,
+ y: isJumping ? -CHARACTER.jumpVelocity : charRigidbody.linvel().y,
+ }
+ const displacement = getDisplacement(
+ velocity,
+ acceleration,
+ 1 / 60,
+ CHARACTER.maxMoveSpeedX,
+ CHARACTER.moveDeceleration,
+ );
+
+ char.computeColliderMovement(
+ charCollider as RAPIER.Collider, // The collider we would like to move.
+ new RAPIER.Vector2(displacement.x, displacement.y),
+ );
+ const correctedDisplacement = char.computedMovement();
+ const currentPos = charRigidbody.translation();
+ const nextX = currentPos.x + correctedDisplacement.x;
+ const nextY = currentPos.y + correctedDisplacement.y;
+ charRigidbody?.setNextKinematicTranslation({ x: nextX, y: nextY });
+
+ const w = charRigidbody.userData.w;
+ const h = charRigidbody.userData.h;
+ this.editor.updateShape({
+ id: charRigidbody.userData.id,
+ type: charRigidbody.userData.type,
+ x: nextX - w / 2,
+ y: nextY - h / 2,
+ });
+ });
+ }
+ private getShapeDimensions(
+ shape: TLShape,
+ ): { width: number; height: number } {
+ const geo = this.editor.getShapeGeometry(shape);
+ const width = geo.center.x * 2;
+ const height = geo.center.y * 2;
+ return { width, height };
+ }
+ private shouldConvexify(shape: TLShape): boolean {
+ return !(
+ shape.type === "geo" && (shape as TLGeoShape).props.geo === "rectangle"
+ );
+ }
+ private createRigidbody(
+ shape: TLShape,
+ gravity = 1,
+ ): RAPIER.RigidBody {
+ const dimensions = this.getShapeDimensions(shape);
+ const centerPosition = cornerToCenter({
+ x: shape.x,
+ y: shape.y,
+ width: dimensions.width,
+ height: dimensions.height,
+ rotation: shape.rotation,
+ });
+ const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()
+ .setTranslation(centerPosition.x, centerPosition.y)
+ .setRotation(shape.rotation)
+ .setGravityScale(gravity);
+ const rigidbody = this.world.createRigidBody(rigidBodyDesc);
+ this.rigidbodyLookup[shape.id] = rigidbody;
+ rigidbody.userData = {
+ id: shape.id,
+ type: shape.type,
+ w: dimensions.width,
+ h: dimensions.height,
+ };
+ return rigidbody;
+ }
+ private createColliderAtPoint(
+ point: VecLike,
+ relativeToParent: TLDrawShape,
+ parentRigidBody: RAPIER.RigidBody | null = null,
+ ) {
+ const radius = 5;
+ const parentGeo = this.editor.getShapeGeometry(relativeToParent);
+ const center = cornerToCenter({
+ x: point.x,
+ y: point.y,
+ width: radius,
+ height: radius,
+ rotation: 0,
+ parent: parentGeo,
+ });
+ let colliderDesc: RAPIER.ColliderDesc | null = null;
+ colliderDesc = RAPIER.ColliderDesc.ball(radius);
+
+ if (!colliderDesc) {
+ console.error("Failed to create collider description.");
+ return;
+ }
+
+ if (parentRigidBody) {
+ colliderDesc.setTranslation(center.x, center.y);
+ this.world.createCollider(colliderDesc, parentRigidBody);
+ } else {
+ colliderDesc.setTranslation(
+ relativeToParent.x + center.x,
+ relativeToParent.y + center.y,
+ );
+ this.world.createCollider(colliderDesc);
+ }
+ }
+ private createCollider(
+ shape: TLShape,
+ parentRigidBody: RAPIER.RigidBody | null = null,
+ parentGeo: Geometry2d | null = null,
+ ) {
+ const dimensions = this.getShapeDimensions(shape);
+ const centerPosition = cornerToCenter({
+ x: shape.x,
+ y: shape.y,
+ width: dimensions.width,
+ height: dimensions.height,
+ rotation: shape.rotation,
+ parent: parentGeo || undefined,
+ });
+
+ const restitution =
+ "color" in shape.props
+ ? getRestitutionFromColor(shape.props.color)
+ : MATERIAL.defaultRestitution;
+ const friction =
+ "color" in shape.props
+ ? getFrictionFromColor(shape.props.color)
+ : MATERIAL.defaultFriction;
+
+ let colliderDesc: RAPIER.ColliderDesc | null = null;
+
+ if (this.shouldConvexify(shape)) {
+ // Convert vertices for convex shapes
+ const vertices = this.editor.getShapeGeometry(shape).vertices;
+ const vec2Array = convertVerticesToFloat32Array(
+ vertices,
+ dimensions.width,
+ dimensions.height,
+ );
+ colliderDesc = RAPIER.ColliderDesc.convexHull(vec2Array);
+ } else {
+ // Cuboid for rectangle shapes
+ colliderDesc = RAPIER.ColliderDesc.cuboid(
+ dimensions.width / 2,
+ dimensions.height / 2,
+ );
+ }
+ if (!colliderDesc) {
+ console.error("Failed to create collider description.");
+ return;
+ }
+
+ colliderDesc
+ .setRestitution(restitution)
+ .setRestitutionCombineRule(RAPIER.CoefficientCombineRule.Max)
+ .setFriction(friction)
+ .setFrictionCombineRule(RAPIER.CoefficientCombineRule.Min);
+ if (parentRigidBody) {
+ if (parentGeo) {
+ colliderDesc.setTranslation(centerPosition.x, centerPosition.y);
+ colliderDesc.setRotation(shape.rotation);
+ }
+ this.world.createCollider(colliderDesc, parentRigidBody);
+ } else {
+ colliderDesc
+ .setTranslation(centerPosition.x, centerPosition.y)
+ .setRotation(shape.rotation);
+ this.world.createCollider(colliderDesc);
+ }
+ }
+ public setEditor(editor: Editor) {
+ this.editor = editor;
+ }
+}
+
+export function usePhysicsSimulation(editor: Editor) {
+ const sim = useRef(new PhysicsWorld(editor));
+
+ useEffect(() => {
+ sim.current.start()
+ }, []);
+
+ useEffect(() => {
+ sim.current.setEditor(editor);
+ }, [editor, sim]);
+
+ // Return any values or functions that the UI components might need
+ return {
+ addShapes: (shapes: TLShape[]) => sim.current.addShapes(shapes),
+ destroy: () => {
+ sim.current.stop()
+ sim.current = new PhysicsWorld(editor); // Replace with a new instance
+ sim.current.start()
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/objects/causal-islands-integration-domain.pdf b/src/public/artifact/causal-islands-integration-domain.pdf
similarity index 100%
rename from src/objects/causal-islands-integration-domain.pdf
rename to src/public/artifact/causal-islands-integration-domain.pdf
diff --git a/src/public/canvas-button.svg b/src/public/canvas-button.svg
new file mode 100644
index 0000000..3325f46
--- /dev/null
+++ b/src/public/canvas-button.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/favicon.ico b/src/public/favicon.ico
similarity index 100%
rename from src/assets/favicon.ico
rename to src/public/favicon.ico
diff --git a/src/public/gravity-button.svg b/src/public/gravity-button.svg
new file mode 100644
index 0000000..5395ac1
--- /dev/null
+++ b/src/public/gravity-button.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/shapes/HTMLShapeUtil.tsx b/src/shapes/HTMLShapeUtil.tsx
new file mode 100644
index 0000000..d62c033
--- /dev/null
+++ b/src/shapes/HTMLShapeUtil.tsx
@@ -0,0 +1,41 @@
+import { Rectangle2d, resizeBox, TLBaseShape, TLOnResizeHandler } from '@tldraw/tldraw';
+import { HTMLContainer, ShapeUtil } from 'tldraw'
+
+export type HTMLShape = TLBaseShape<'html', { w: number; h: number, html: string }>
+
+export class HTMLShapeUtil extends ShapeUtil {
+ static override type = 'html' as const
+ override canBind = () => true
+ override canEdit = () => false
+ override canResize = () => true
+ override isAspectRatioLocked = () => false
+
+ getDefaultProps(): HTMLShape['props'] {
+ return {
+ w: 100,
+ h: 100,
+ html: "
"
+ }
+ }
+
+ override onResize: TLOnResizeHandler = (shape, info) => {
+ return resizeBox(shape, info)
+ }
+
+ getGeometry(shape: HTMLShape) {
+ return new Rectangle2d({
+ width: shape.props.w,
+ height: shape.props.h,
+ isFilled: true,
+ })
+ }
+
+ component(shape: HTMLShape) {
+ return
+
+ }
+
+ indicator(shape: HTMLShape) {
+ return
+ }
+}
\ No newline at end of file
diff --git a/src/utils.tsx b/src/utils.tsx
new file mode 100644
index 0000000..04e80dc
--- /dev/null
+++ b/src/utils.tsx
@@ -0,0 +1,35 @@
+import { createShapeId } from "@tldraw/tldraw";
+
+export function createShapes(elementsInfo: any) {
+ const shapes = elementsInfo.map((element: any) => ({
+ id: createShapeId(),
+ type: 'html',
+ x: element.x,
+ y: element.y,
+ props: {
+ w: element.w,
+ h: element.h,
+ html: element.html,
+ }
+ }));
+
+ const extend = 150;
+
+ shapes.push({
+ id: createShapeId(),
+ type: 'geo',
+ x: -extend,
+ y: window.innerHeight,
+ props: {
+ w: window.innerWidth + (extend * 2),
+ h: 50,
+ color: 'grey',
+ fill: 'solid'
+ },
+ meta: {
+ fixed: true
+ }
+ });
+
+ return shapes;
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ae3bf28
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "paths": {
+ "@/*": ["*"],
+ "src/*": ["./src/*"],
+ },
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vercel.json b/vercel.json
index ec27fad..7c98098 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,5 +1,5 @@
{
"buildCommand": "yarn build",
- "framework": "eleventy",
+ "framework": "vite",
"outputDirectory": "dist"
}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..9b55a11
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import wasm from "vite-plugin-wasm";
+import topLevelAwait from "vite-plugin-top-level-await";
+
+export default defineConfig({
+ plugins: [
+ react(),
+ wasm(),
+ topLevelAwait()
+ ],
+ build: {
+ sourcemap: true, // Enable source maps for production
+ },
+ publicDir: 'src/public',
+ resolve: {
+ alias: {
+ '@': '/src',
+ },
+ },
+})