Merge pull request #2 from OrionReed/goofy-physics-button
add more goof
This commit is contained in:
commit
17bc2f3d0d
13
.eleventy.js
13
.eleventy.js
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Orion Reed</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="src/public/favicon.ico?v=4" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="src/public/favicon.ico?v=4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/App.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
package.json
22
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(<App />);
|
||||
|
||||
function App() {
|
||||
|
||||
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/card/contact" element={<Contact />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<><Toggle />
|
||||
<div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}>
|
||||
{<Default />}
|
||||
</div>
|
||||
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</>)
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Orion Reed</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico?v=4" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/assets/favicon.ico?v=4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
Orion Reed
|
||||
</header>
|
||||
<main>
|
||||
{{ content }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
components={components}
|
||||
shapeUtils={[HTMLShapeUtil]}
|
||||
onMount={() => {
|
||||
window.dispatchEvent(new CustomEvent('editorDidMountEvent'));
|
||||
}}
|
||||
>
|
||||
<SimController shapes={shapes} />
|
||||
</Tldraw>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export function Contact() {
|
||||
return (
|
||||
<main>
|
||||
<header>
|
||||
<a href="/">
|
||||
Orion Reed
|
||||
</a>
|
||||
</header>
|
||||
<h1>Contact</h1>
|
||||
<p>Twitter: <a href="https://twitter.com/OrionReedOne">@OrionReedOne</a></p>
|
||||
<p>Mastodon: <a href="https://hci.social/@orion">orion@hci.social</a></p>
|
||||
<p>Email: <a href="mailto:me@orionreed.com">me@orionreed.com</a></p>
|
||||
<p>GitHub: <a href="https://github.com/orionreed">OrionReed</a></p>
|
||||
</main>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
export function Default() {
|
||||
return (
|
||||
<main>
|
||||
<header>
|
||||
Orion Reed
|
||||
</header>
|
||||
<h1>Hello! 👋</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h1>My work</h1>
|
||||
<p>
|
||||
Alongside my independent work I am a researcher at <a href="https://block.science/">Block Science</a> building{' '}
|
||||
<i>knowledge organisation infrastructure</i> and at <a href="https://economicspace.agency/">ECSA</a> working on{' '}
|
||||
<i>computational media</i>. I am also part of the nascent <a href="https://libcomp.org/">Liberatory Computing</a>{' '}
|
||||
collective and a co-organiser of the <a href="https://canvasprotocol.org/">OCWG</a>.
|
||||
</p>
|
||||
|
||||
<h1>Get in touch</h1>
|
||||
<p>
|
||||
I am on Twitter as <a href="https://twitter.com/OrionReedOne">@OrionReedOne</a> and on
|
||||
Mastodon as <a href="https://hci.social/@orion">@orion@hci.social</a>. The best way to reach me is
|
||||
through Twitter or my email, <a href="mailto:me@orionreed.com">me@orionreed.com</a>
|
||||
</p>
|
||||
|
||||
<span className="dinkus">***</span>
|
||||
|
||||
<h1>Talks</h1>
|
||||
<ul>
|
||||
<li><a
|
||||
href="artifact/causal-islands-integration-domain.pdf">Spatial
|
||||
Canvases: Towards an Integration Domain for HCI @ Causal Islands LA</a></li>
|
||||
<li><a
|
||||
href="https://www.youtube.com/watch?v=-q-kk-NMFbA">Knowledge Organisation Infrastructure Demo @ NPC
|
||||
Denver</a></li>
|
||||
</ul>
|
||||
<h1>Writing</h1>
|
||||
<ul>
|
||||
<li><a
|
||||
href="https://blog.block.science/objects-as-reference-toward-robust-first-principles-of-digital-organization/">Objects
|
||||
as Reference: Toward Robust First Principles of Digital Organization</a></li>
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export function Toggle() {
|
||||
return (
|
||||
<>
|
||||
<button id="toggle-canvas" onClick={() => window.dispatchEvent(new CustomEvent('toggleCanvasEvent'))}>
|
||||
<img src="/canvas-button.svg" alt="Toggle Canvas" />
|
||||
</button>
|
||||
<button id="toggle-physics" className="hidden" onClick={() => window.dispatchEvent(new CustomEvent('togglePhysicsEvent'))}>
|
||||
<img src="/gravity-button.svg" alt="Toggle Physics" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<ElementInfo[]>([]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
layout: layout.html
|
||||
---
|
||||
|
||||
<h1>Hello! 👋</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h1>My work</h1>
|
||||
<p>
|
||||
Alongside my independent work I am a researcher at
|
||||
<a href="https://block.science/">Block Science</a> building
|
||||
<i>knowledge organisation infrastructure</i> and at
|
||||
<a href="https://economicspace.agency/">ECSA</a> working on
|
||||
<i>computational media</i>. I am also part of the nascent <a href="https://libcomp.org/">Liberatory Computing</a>
|
||||
collective and a co-organiser of the <a href="https://canvasprotocol.org/">OCWG</a>.
|
||||
</p>
|
||||
|
||||
<h1>Get in touch</h1>
|
||||
<p>
|
||||
I am on Twitter as <a href="https://twitter.com/OrionReedOne">@OrionReedOne</a> and on
|
||||
Mastodon as <a href="https://hci.social/@orion">@orion@hci.social</a>. The best way to reach me is
|
||||
through Twitter or my email, <a href="mailto:me@orionreed.com">me@orionreed.com</a>
|
||||
</p>
|
||||
|
||||
<span class="dinkus">***</span>
|
||||
|
||||
<h1>Talks</h1>
|
||||
<ul>
|
||||
<li><a
|
||||
href="objects/causal-islands-integration-domain.pdf">Spatial
|
||||
Canvases: Towards an Integration Domain for HCI @ Causal Islands LA</a></li>
|
||||
<li><a
|
||||
href="https://www.youtube.com/watch?v=-q-kk-NMFbA">Knowledge Organisation Infrastructure Demo @ NPC Denver</a></li>
|
||||
</ul>
|
||||
<h1>Writing</h1>
|
||||
<ul>
|
||||
<li><a
|
||||
href="https://blog.block.science/objects-as-reference-toward-robust-first-principles-of-digital-organization/">Objects
|
||||
as Reference: Toward Robust First Principles of Digital Organization</a></li>
|
||||
</ul>
|
||||
|
|
@ -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 (<></>);
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<PhysicsWorld>(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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.5.6 -->
|
||||
<svg width="450" height="450" viewBox="0 0 450 450" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rounded-Rectangle" fill="none" stroke="#000000" stroke-width="34" stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M 112.785156 427.964844 C 79.383881 427.964844 62.682236 427.963867 50.160156 420.951172 C 41.310253 415.994995 34.005013 408.689758 29.048828 399.839844 C 22.036131 387.317749 22.035156 370.616119 22.035156 337.214844 L 22.035156 298.828125 L 40.128906 298.828125 L 40.128906 350.470703 C 40.128906 372.333344 40.128616 383.264679 44.71875 391.460938 C 47.962795 397.253601 52.746403 402.037201 58.539063 405.28125 C 66.735329 409.871368 77.666649 409.871094 99.529297 409.871094 L 151.171875 409.871094 L 151.171875 427.964844 L 112.785156 427.964844 Z M 297.949219 427.964844 L 297.949219 409.871094 L 349.599609 409.871094 C 371.462219 409.871094 382.393555 409.871399 390.589844 405.28125 C 396.382538 402.037201 401.166107 397.253601 404.410156 391.460938 C 409.000305 383.264679 409 372.333344 409 350.470703 L 409 298.828125 L 427.09375 298.828125 L 427.09375 337.214844 C 427.09375 370.616119 427.092804 387.317749 420.080078 399.839844 C 415.123901 408.689758 407.818665 415.994995 398.96875 420.951172 C 386.446625 427.963867 369.745026 427.964844 336.34375 427.964844 L 297.949219 427.964844 Z M 22.035156 152.050781 L 22.035156 113.65625 C 22.035156 80.254974 22.036131 63.553375 29.048828 51.03125 C 34.005013 42.181335 41.310253 34.876129 50.160156 29.919922 C 62.682236 22.907196 79.383881 22.90625 112.785156 22.90625 L 151.171875 22.90625 L 151.171875 41 L 99.529297 41 C 77.666649 41 66.735329 40.999695 58.539063 45.589844 C 52.746407 48.833893 47.962795 53.617493 44.71875 59.410156 C 40.12862 67.606445 40.128906 78.537781 40.128906 100.400391 L 40.128906 152.050781 L 22.035156 152.050781 Z M 409 152.050781 L 409 100.400391 C 409 78.537781 409.000305 67.606445 404.410156 59.410156 C 401.166107 53.617462 396.382507 48.833893 390.589844 45.589844 C 382.393555 40.999695 371.462219 41 349.599609 41 L 297.949219 41 L 297.949219 22.90625 L 336.34375 22.90625 C 369.745026 22.90625 386.446625 22.907196 398.96875 29.919922 C 407.818665 34.876099 415.123871 42.181335 420.080078 51.03125 C 427.092804 63.553375 427.09375 80.254974 427.09375 113.65625 L 427.09375 152.050781 L 409 152.050781 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="gravity" data-name="Line Color" xmlns="http://www.w3.org/2000/svg" class="icon line-color">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path id="secondary" d="M12,3V9m4-5v6M8,4v6" style="fill: none; stroke: #000000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"/>
|
||||
<circle id="primary" cx="12" cy="17" r="4" style="fill: none; stroke: #000000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 816 B |
|
|
@ -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<HTMLShape> {
|
||||
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: "<div></div>"
|
||||
}
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<any> = (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 <div dangerouslySetInnerHTML={{ __html: shape.props.html }}></div>
|
||||
|
||||
}
|
||||
|
||||
indicator(shape: HTMLShape) {
|
||||
return <rect width={shape.props.w} height={shape.props.h} />
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"buildCommand": "yarn build",
|
||||
"framework": "eleventy",
|
||||
"framework": "vite",
|
||||
"outputDirectory": "dist"
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue