From 5bc074b33a2fe4f79dd291c102df04c2908c1327 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 18:07:52 -0700 Subject: [PATCH 01/13] switch back to React --- .eleventy.js | 13 - __________src/default_store.ts | 141 ++++++++++ index.html | 73 ++++++ package.json | 23 +- src/App.tsx | 17 ++ src/_includes/layout.html | 24 -- src/css/index.css | 64 +++++ src/{assets => }/css/reset.css | 0 src/{assets => }/css/style.css | 0 src/index.html | 53 ---- src/main.tsx | 10 + src/physics/config.ts | 36 +++ src/physics/math.ts | 95 +++++++ src/physics/simulation.ts | 395 +++++++++++++++++++++++++++++ src/physics/ui/PhysicsControls.tsx | 47 ++++ src/physics/ui/overrides.ts | 22 ++ src/vite-env.d.ts | 1 + tsconfig.json | 25 ++ tsconfig.node.json | 10 + vercel.json | 2 +- vite.config.ts | 12 + 21 files changed, 969 insertions(+), 94 deletions(-) delete mode 100644 .eleventy.js create mode 100644 __________src/default_store.ts create mode 100644 index.html create mode 100644 src/App.tsx delete mode 100644 src/_includes/layout.html create mode 100644 src/css/index.css rename src/{assets => }/css/reset.css (100%) rename src/{assets => }/css/style.css (100%) delete mode 100644 src/index.html create mode 100644 src/main.tsx create mode 100644 src/physics/config.ts create mode 100644 src/physics/math.ts create mode 100644 src/physics/simulation.ts create mode 100644 src/physics/ui/PhysicsControls.tsx create mode 100644 src/physics/ui/overrides.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts 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/__________src/default_store.ts b/__________src/default_store.ts new file mode 100644 index 0000000..98176bb --- /dev/null +++ b/__________src/default_store.ts @@ -0,0 +1,141 @@ +export const DEFAULT_STORE = { + store: { + "document:document": { + gridSize: 10, + name: "", + meta: {}, + id: "document:document", + typeName: "document", + }, + "pointer:pointer": { + id: "pointer:pointer", + typeName: "pointer", + x: 0, + y: 0, + lastActivityTimestamp: 0, + meta: {}, + }, + "page:page": { + meta: {}, + id: "page:page", + name: "Page 1", + index: "a1", + typeName: "page", + }, + "camera:page:page": { + x: 0, + y: 0, + z: 1, + meta: {}, + id: "camera:page:page", + typeName: "camera", + }, + "instance_page_state:page:page": { + editingShapeId: null, + croppingShapeId: null, + selectedShapeIds: [], + hoveredShapeId: null, + erasingShapeIds: [], + hintingShapeIds: [], + focusedGroupId: null, + meta: {}, + id: "instance_page_state:page:page", + pageId: "page:page", + typeName: "instance_page_state", + }, + "instance:instance": { + followingUserId: null, + opacityForNextShape: 1, + stylesForNextShape: {}, + brush: null, + scribble: null, + cursor: { + type: "default", + rotation: 0, + }, + isFocusMode: false, + exportBackground: true, + isDebugMode: false, + isToolLocked: false, + screenBounds: { + x: 0, + y: 0, + w: 720, + h: 400, + }, + zoomBrush: null, + isGridMode: false, + isPenMode: false, + chatMessage: "", + isChatting: false, + highlightedUserIds: [], + canMoveCamera: true, + isFocused: true, + devicePixelRatio: 2, + isCoarsePointer: false, + isHoveringCanvas: false, + openMenus: [], + isChangingStyle: false, + isReadonly: false, + meta: {}, + id: "instance:instance", + currentPageId: "page:page", + typeName: "instance", + }, + }, + schema: { + schemaVersion: 1, + storeVersion: 4, + recordVersions: { + asset: { + version: 1, + subTypeKey: "type", + subTypeVersions: { + image: 2, + video: 2, + bookmark: 0, + }, + }, + camera: { + version: 1, + }, + document: { + version: 2, + }, + instance: { + version: 21, + }, + instance_page_state: { + version: 5, + }, + page: { + version: 1, + }, + shape: { + version: 3, + subTypeKey: "type", + subTypeVersions: { + group: 0, + text: 1, + bookmark: 1, + draw: 1, + geo: 7, + note: 4, + line: 1, + frame: 0, + arrow: 1, + highlight: 0, + embed: 4, + image: 2, + video: 1, + }, + }, + instance_presence: { + version: 5, + }, + pointer: { + version: 1, + }, + }, + }, +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..90be5ff --- /dev/null +++ b/index.html @@ -0,0 +1,73 @@ + + + + Orion Reed + + + + + + + + + + +
+ Orion Reed +
+
+

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

+ +
+ + \ No newline at end of file diff --git a/package.json b/package.json index 1d4c110..d99d5db 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,30 @@ "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": "vite preview" }, "keywords": [], "author": "Orion Reed", "license": "ISC", + "dependencies": { + "@dimforge/rapier2d": "latest", + "@tldraw/tldraw": "2.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, "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", + "concurrently": "^8.2.0", + "gh-pages": "^6.1.1", + "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..49d4cc1 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,17 @@ +import { Tldraw, track, useEditor } from "@tldraw/tldraw"; +import "@tldraw/tldraw/tldraw.css"; +import { SimControls } from "./physics/ui/PhysicsControls"; +import { uiOverrides } from "./physics/ui/overrides"; + +export default function Canvas() { + + return ( +
+ + + +
+ ); +} 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 - - - - - - - - - - -
- Orion Reed -
-
- {{ content }} -
- - \ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css new file mode 100644 index 0000000..2f8e1a1 --- /dev/null +++ b/src/css/index.css @@ -0,0 +1,64 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); + +html, +body { + padding: 0; + margin: 0; + font-family: "Inter", sans-serif; + overscroll-behavior: none; + touch-action: none; + min-height: 100vh; + font-size: 16px; + /* mobile viewport bug fix */ + min-height: -webkit-fill-available; + height: 100%; +} + +html, +* { + box-sizing: border-box; +} + +.tldraw__editor { + position: fixed; + inset: 0px; + overflow: hidden; +} + +.examples { + padding: 16px; +} + +.examples__header { + width: fit-content; + padding-bottom: 32px; +} + +.examples__lockup { + height: 56px; + width: auto; +} + +.examples__list { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + list-style: none; +} + +.examples__list__item { + padding: 8px 12px; + margin: 0px -12px; +} + +.examples__list__item a { + padding: 8px 12px; + margin: 0px -12px; + text-decoration: none; + color: inherit; +} + +.examples__list__item a:hover { + text-decoration: underline; +} 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/assets/css/style.css b/src/css/style.css similarity index 100% rename from src/assets/css/style.css rename to src/css/style.css 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/main.tsx b/src/main.tsx new file mode 100644 index 0000000..07e30df --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import "./css/index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); 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..f6ec6b8 --- /dev/null +++ b/src/physics/simulation.ts @@ -0,0 +1,395 @@ +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); + + this.addShapes(this.editor.getSelectedShapes()); + + 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[]) { + for (const shape of shapes) { + if ('color' in shape.props && shape.props.color === "violet") { + this.createCharacter(shape as TLGeoShape); + continue; + } + + switch (shape.type) { + 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.props.dash === "dashed") return; // Skip dashed shapes + if (isRigidbody(shape.props.color)) { + const gravity = getGravityFromColor(shape.props.color) + const rb = this.createRigidbody(shape, gravity); + 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); + 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, enabled: boolean) { + const sim = useRef(new PhysicsWorld(editor)); + + useEffect(() => { + if (enabled) { + sim.current.start(); + editor.selectNone(); + return () => sim.current.stop(); + } + }, [enabled, sim]); + + 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), + }; +} \ No newline at end of file diff --git a/src/physics/ui/PhysicsControls.tsx b/src/physics/ui/PhysicsControls.tsx new file mode 100644 index 0000000..2ccf828 --- /dev/null +++ b/src/physics/ui/PhysicsControls.tsx @@ -0,0 +1,47 @@ +import { track, useEditor } from "@tldraw/tldraw"; +import { useEffect, useState } from "react"; +import { usePhysicsSimulation } from "../simulation"; +import "../../css/physics-ui.css"; + +export const SimControls = track(() => { + const editor = useEditor(); + const [physicsEnabled, setPhysics] = useState(false); + + useEffect(() => { + const togglePhysics = () => { + setPhysics(prev => !prev); + }; + + window.addEventListener('togglePhysicsEvent', togglePhysics); + + return () => { + window.removeEventListener('togglePhysicsEvent', togglePhysics); + }; + }, []); + + const { addShapes } = usePhysicsSimulation(editor, physicsEnabled); + + return ( +
+
+ + +
+
+ ); +}); diff --git a/src/physics/ui/overrides.ts b/src/physics/ui/overrides.ts new file mode 100644 index 0000000..ae905e7 --- /dev/null +++ b/src/physics/ui/overrides.ts @@ -0,0 +1,22 @@ +import { + TLUiEventSource, + TLUiOverrides, + TLUiTranslationKey, +} from "@tldraw/tldraw"; + +// In order to see select our custom shape tool, we need to add it to the ui. +export const uiOverrides: TLUiOverrides = { + actions(_editor, actions) { + actions['toggle-physics'] = { + id: 'toggle-physics', + label: 'Toggle Physics' as TLUiTranslationKey, + readonlyOk: true, + kbd: 'p', + onSelect(_source: TLUiEventSource) { + const event = new CustomEvent('togglePhysicsEvent'); + window.dispatchEvent(event); + }, + } + return actions + }, +} \ 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..54de499 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "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..7726f73 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +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() + ], +}) From 8c3dec7271ddeb4fa86c62258851a3249c23bf73 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 18:18:36 -0700 Subject: [PATCH 02/13] added button --- index.html | 3 +++ src/assets/gravity.svg | 10 ++++++++++ src/card/contact.html | 7 +++++++ src/card/contact.md | 10 ---------- src/css/style.css | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/assets/gravity.svg create mode 100644 src/card/contact.html delete mode 100644 src/card/contact.md diff --git a/index.html b/index.html index 90be5ff..3c1fd8a 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,9 @@
Orion Reed
+

Hello! 👋

diff --git a/src/assets/gravity.svg b/src/assets/gravity.svg new file mode 100644 index 0000000..5395ac1 --- /dev/null +++ b/src/assets/gravity.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/card/contact.html b/src/card/contact.html new file mode 100644 index 0000000..f157b8c --- /dev/null +++ b/src/card/contact.html @@ -0,0 +1,7 @@ +

+

Contact

+

Twitter: @OrionReedOne

+

Mastodon: orion@hci.social

+

Email: me@orionreed.com

+

GitHub: OrionReed

+
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/css/style.css b/src/css/style.css index 8926a69..78176f0 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -43,6 +43,22 @@ p { margin-top: 2em; margin-bottom: 0em; } + +#toggle-physics { + position: absolute; + top: 10px; + right: 10px; + width: 50px; + height: 50px; + background: none; + border: none; + cursor: pointer; + opacity: 0.5; + &:hover { + opacity: 1; + } +} + ul { padding-left: 0; list-style: decimal-leading-zero; From 52383179c18cfa7689c834e02b4c1bde4b063bb3 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 20:06:27 -0700 Subject: [PATCH 03/13] Add canvas --- __________src/default_store.ts | 141 ----------------------------- index.html | 62 +------------ package.json | 6 +- src/App.tsx | 117 +++++++++++++++++++++++- src/css/{style.css => default.css} | 15 --- src/css/index.css | 64 ------------- src/css/tldraw.css | 30 ++++++ src/css/toggle.css | 19 ++++ src/main.tsx | 10 -- src/physics/ui/PhysicsControls.tsx | 2 +- 10 files changed, 170 insertions(+), 296 deletions(-) delete mode 100644 __________src/default_store.ts rename src/css/{style.css => default.css} (83%) delete mode 100644 src/css/index.css create mode 100644 src/css/tldraw.css create mode 100644 src/css/toggle.css delete mode 100644 src/main.tsx diff --git a/__________src/default_store.ts b/__________src/default_store.ts deleted file mode 100644 index 98176bb..0000000 --- a/__________src/default_store.ts +++ /dev/null @@ -1,141 +0,0 @@ -export const DEFAULT_STORE = { - store: { - "document:document": { - gridSize: 10, - name: "", - meta: {}, - id: "document:document", - typeName: "document", - }, - "pointer:pointer": { - id: "pointer:pointer", - typeName: "pointer", - x: 0, - y: 0, - lastActivityTimestamp: 0, - meta: {}, - }, - "page:page": { - meta: {}, - id: "page:page", - name: "Page 1", - index: "a1", - typeName: "page", - }, - "camera:page:page": { - x: 0, - y: 0, - z: 1, - meta: {}, - id: "camera:page:page", - typeName: "camera", - }, - "instance_page_state:page:page": { - editingShapeId: null, - croppingShapeId: null, - selectedShapeIds: [], - hoveredShapeId: null, - erasingShapeIds: [], - hintingShapeIds: [], - focusedGroupId: null, - meta: {}, - id: "instance_page_state:page:page", - pageId: "page:page", - typeName: "instance_page_state", - }, - "instance:instance": { - followingUserId: null, - opacityForNextShape: 1, - stylesForNextShape: {}, - brush: null, - scribble: null, - cursor: { - type: "default", - rotation: 0, - }, - isFocusMode: false, - exportBackground: true, - isDebugMode: false, - isToolLocked: false, - screenBounds: { - x: 0, - y: 0, - w: 720, - h: 400, - }, - zoomBrush: null, - isGridMode: false, - isPenMode: false, - chatMessage: "", - isChatting: false, - highlightedUserIds: [], - canMoveCamera: true, - isFocused: true, - devicePixelRatio: 2, - isCoarsePointer: false, - isHoveringCanvas: false, - openMenus: [], - isChangingStyle: false, - isReadonly: false, - meta: {}, - id: "instance:instance", - currentPageId: "page:page", - typeName: "instance", - }, - }, - schema: { - schemaVersion: 1, - storeVersion: 4, - recordVersions: { - asset: { - version: 1, - subTypeKey: "type", - subTypeVersions: { - image: 2, - video: 2, - bookmark: 0, - }, - }, - camera: { - version: 1, - }, - document: { - version: 2, - }, - instance: { - version: 21, - }, - instance_page_state: { - version: 5, - }, - page: { - version: 1, - }, - shape: { - version: 3, - subTypeKey: "type", - subTypeVersions: { - group: 0, - text: 1, - bookmark: 1, - draw: 1, - geo: 7, - note: 4, - line: 1, - frame: 0, - arrow: 1, - highlight: 0, - embed: 4, - image: 2, - video: 1, - }, - }, - instance_presence: { - version: 5, - }, - pointer: { - version: 1, - }, - }, - }, -}; diff --git a/index.html b/index.html index 3c1fd8a..d37cc60 100644 --- a/index.html +++ b/index.html @@ -8,69 +8,11 @@ - - -
- Orion Reed -
- -
-

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

- -
+ + \ No newline at end of file diff --git a/package.json b/package.json index d99d5db..3556c24 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "dependencies": { "@dimforge/rapier2d": "latest", "@tldraw/tldraw": "2.0.2", + "@types/react-helmet": "^6.1.11", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-helmet": "^6.1.0" }, "devDependencies": { "@biomejs/biome": "1.4.1", @@ -30,4 +32,4 @@ "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 index 49d4cc1..6ec1d08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,128 @@ -import { Tldraw, track, useEditor } from "@tldraw/tldraw"; +import { createTLUser, setUserPreferences, Tldraw, track, useEditor } from "@tldraw/tldraw"; import "@tldraw/tldraw/tldraw.css"; import { SimControls } from "./physics/ui/PhysicsControls"; import { uiOverrides } from "./physics/ui/overrides"; +import { Helmet } from "react-helmet"; +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom/client"; -export default function Canvas() { +ReactDOM.createRoot(document.getElementById("root")!).render(); + +function App() { + const [isPhysicsEnabled, setIsPhysicsEnabled] = useState(false); + + useEffect(() => { + const togglePhysics = () => setIsPhysicsEnabled(prev => !prev); + + window.addEventListener('togglePhysicsEvent', togglePhysics); + + return () => { + window.removeEventListener('togglePhysicsEvent', togglePhysics); + }; + }, []); + + return ( + + + {isPhysicsEnabled ? : } + + ); +}; + +function Default() { + return ( + <> + + + +
+ Orion Reed +
+

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

+ + + ); +} + +function Canvas() { return (
+ + + - + {/* */} + {/* {()=> { + setUserPreferences({id: 'orion', isDarkMode: true }) + }} */} +
); } + +function Toggle() { + return ( + <> + + + + + + ); +} + diff --git a/src/css/style.css b/src/css/default.css similarity index 83% rename from src/css/style.css rename to src/css/default.css index 78176f0..7a6da37 100644 --- a/src/css/style.css +++ b/src/css/default.css @@ -44,21 +44,6 @@ p { margin-bottom: 0em; } -#toggle-physics { - position: absolute; - top: 10px; - right: 10px; - width: 50px; - height: 50px; - background: none; - border: none; - cursor: pointer; - opacity: 0.5; - &:hover { - opacity: 1; - } -} - ul { padding-left: 0; list-style: decimal-leading-zero; diff --git a/src/css/index.css b/src/css/index.css deleted file mode 100644 index 2f8e1a1..0000000 --- a/src/css/index.css +++ /dev/null @@ -1,64 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); - -html, -body { - padding: 0; - margin: 0; - font-family: "Inter", sans-serif; - overscroll-behavior: none; - touch-action: none; - min-height: 100vh; - font-size: 16px; - /* mobile viewport bug fix */ - min-height: -webkit-fill-available; - height: 100%; -} - -html, -* { - box-sizing: border-box; -} - -.tldraw__editor { - position: fixed; - inset: 0px; - overflow: hidden; -} - -.examples { - padding: 16px; -} - -.examples__header { - width: fit-content; - padding-bottom: 32px; -} - -.examples__lockup { - height: 56px; - width: auto; -} - -.examples__list { - display: flex; - flex-direction: column; - padding: 0; - margin: 0; - list-style: none; -} - -.examples__list__item { - padding: 8px 12px; - margin: 0px -12px; -} - -.examples__list__item a { - padding: 8px 12px; - margin: 0px -12px; - text-decoration: none; - color: inherit; -} - -.examples__list__item a:hover { - text-decoration: underline; -} diff --git a/src/css/tldraw.css b/src/css/tldraw.css new file mode 100644 index 0000000..6b5cb6e --- /dev/null +++ b/src/css/tldraw.css @@ -0,0 +1,30 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); + +html, +body { + padding: 0; + margin: 0; + font-family: "Inter", sans-serif; + overscroll-behavior: none; + touch-action: none; + min-height: 100vh; + /* font-size: 16px; */ + /* mobile viewport bug fix */ + min-height: -webkit-fill-available; + height: 100%; +} + +html, +* { + box-sizing: border-box; +} + +.tldraw__editor { + position: fixed; + inset: 0px; + overflow: hidden; +} + +.tl-background { + background-color: transparent !important; +} diff --git a/src/css/toggle.css b/src/css/toggle.css new file mode 100644 index 0000000..94f0ab1 --- /dev/null +++ b/src/css/toggle.css @@ -0,0 +1,19 @@ +#toggle-physics { + position: absolute; + z-index: 99999; + top: 10px; + right: 10px; + width: 50px; + height: 50px; + background: none; + border: none; + cursor: pointer; + opacity: 0.25; + &:hover { + opacity: 1; + } + & img { + width: 100%; + height: 100%; + } +} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 07e30df..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; -import "./css/index.css"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/src/physics/ui/PhysicsControls.tsx b/src/physics/ui/PhysicsControls.tsx index 2ccf828..477e264 100644 --- a/src/physics/ui/PhysicsControls.tsx +++ b/src/physics/ui/PhysicsControls.tsx @@ -1,7 +1,7 @@ import { track, useEditor } from "@tldraw/tldraw"; import { useEffect, useState } from "react"; import { usePhysicsSimulation } from "../simulation"; -import "../../css/physics-ui.css"; +// import "../../css/physics-ui.css"; export const SimControls = track(() => { const editor = useEditor(); From 9c50ec8d66ed03e0516485ee619dc98606feb7e7 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 22:10:30 -0700 Subject: [PATCH 04/13] translate DOM to shapes --- index.html | 3 +- package.json | 5 +- src/App.tsx | 158 +++++++++++++++++++++++++---- src/card/contact.html | 7 -- src/css/tldraw.css | 6 +- src/physics/simulation.ts | 2 +- src/physics/ui/PhysicsControls.tsx | 48 ++------- 7 files changed, 159 insertions(+), 70 deletions(-) delete mode 100644 src/card/contact.html diff --git a/index.html b/index.html index d37cc60..1d4d89f 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,8 @@ 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"> - + +
\ No newline at end of file diff --git a/package.json b/package.json index 3556c24..a236846 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,9 @@ "dependencies": { "@dimforge/rapier2d": "latest", "@tldraw/tldraw": "2.0.2", - "@types/react-helmet": "^6.1.11", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-helmet": "^6.1.0" + "react-helmet-async": "^2.0.4" }, "devDependencies": { "@biomejs/biome": "1.4.1", @@ -32,4 +31,4 @@ "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 index 6ec1d08..cff4c4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,119 @@ -import { createTLUser, setUserPreferences, Tldraw, track, useEditor } from "@tldraw/tldraw"; +import { atom, createShapeId, createTLUser, setUserPreferences, StoreSnapshot, Tldraw, TLGeoShape, TLInstance, TLRecord, TLShape, TLUiComponents, TLUnknownShape, TLUserPreferences, track, useEditor } from "@tldraw/tldraw"; import "@tldraw/tldraw/tldraw.css"; import { SimControls } from "./physics/ui/PhysicsControls"; import { uiOverrides } from "./physics/ui/overrides"; -import { Helmet } from "react-helmet"; -import React, { useEffect, useState } from "react"; +import { Helmet, HelmetProvider } from "react-helmet-async"; +import React, { Suspense, useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; ReactDOM.createRoot(document.getElementById("root")!).render(); +const components: TLUiComponents = { + HelpMenu: null, + StylePanel: null, + PageMenu: null, + NavigationPanel: null, + DebugMenu: null, + MenuPanel: null, + // ContextMenu: null, + // ActionsMenu: null, + // ZoomMenu: null, + // MainMenu: null, + // Minimap: null, + // Toolbar: null, + // KeyboardShortcutsDialog: null, + // QuickActions: null, + // HelperButtons: null, + // SharePanel: null, + // TopPanel: null, +} + function App() { const [isPhysicsEnabled, setIsPhysicsEnabled] = useState(false); + const [elementsInfo, setElementsInfo] = useState([]); useEffect(() => { - const togglePhysics = () => setIsPhysicsEnabled(prev => !prev); + const togglePhysics = async () => { + if (!isPhysicsEnabled) { + const info = await gatherElementsInfo(); + setElementsInfo(info); + setIsPhysicsEnabled(true); // Enable physics only after gathering info + } else { + setIsPhysicsEnabled(false); + setElementsInfo([]); // Reset elements info when disabling physics + } + }; window.addEventListener('togglePhysicsEvent', togglePhysics); return () => { window.removeEventListener('togglePhysicsEvent', togglePhysics); }; - }, []); + }, [isPhysicsEnabled]); + + // Function to gather elements info asynchronously + async function gatherElementsInfo() { + const rootElement = document.getElementById('root'); + const info: any[] = []; + if (rootElement) { + for (const child of rootElement.children) { + const rect = child.getBoundingClientRect(); + let w = rect.width + if (!['P', 'UL'].includes(child.tagName)) { + w = measureElementTextWidth(child); + } + console.log(w) + info.push({ + tagName: child.tagName, + position: { x: rect.left, y: rect.top }, + dimensions: { width: w, height: rect.height }, + }); + }; + } + // Example usage + // const element = document.getElementById('yourElementId'); // Replace 'yourElementId' with the actual ID + // if (element) { + // console.log(`Text width: ${textWidth}px`); + // } + // console.log(info.length); + // console.log(info); + + return info; + } + + const shapes: TLGeoShape[] = elementsInfo.map((element, index) => ({ + id: createShapeId(), + type: 'geo', + x: element.position.x, + y: element.position.y, + props: { + geo: "rectangle", + w: element.dimensions.width, + h: element.dimensions.height, + fill: 'solid', + color: 'green' + } + })) + + shapes.push({ + id: createShapeId(), + type: 'geo', + x: 0, + y: window.innerHeight, + props: { + geo: "rectangle", + w: window.innerWidth, + h: 20, + fill: 'solid' + } + }) return ( - - {isPhysicsEnabled ? : } + + + {isPhysicsEnabled && elementsInfo.length > 0 ? : } + ); }; @@ -56,10 +145,8 @@ function Default() {

My work

- Alongside my independent work I am a researcher at - Block Science building - knowledge organisation infrastructure and at - ECSA working on + 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.

@@ -92,7 +179,7 @@ function Default() { ); } -function Canvas() { +function Canvas({ shapes }: { shapes: TLShape[] }) { return (
@@ -101,13 +188,9 @@ function Canvas() { - {/* */} - {/* {()=> { - setUserPreferences({id: 'orion', isDarkMode: true }) - }} */} - +
); @@ -126,3 +209,42 @@ function Toggle() { ); } +function Contact() { + return ( +
+

Contact

+

Twitter: @OrionReedOne

+

Mastodon: orion@hci.social

+

Email: me@orionreed.com

+

GitHub: OrionReed

+
+ + ) +} + +function measureElementTextWidth(element: Element) { + // 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/card/contact.html b/src/card/contact.html deleted file mode 100644 index f157b8c..0000000 --- a/src/card/contact.html +++ /dev/null @@ -1,7 +0,0 @@ -
-

Contact

-

Twitter: @OrionReedOne

-

Mastodon: orion@hci.social

-

Email: me@orionreed.com

-

GitHub: OrionReed

-
diff --git a/src/css/tldraw.css b/src/css/tldraw.css index 6b5cb6e..b938a37 100644 --- a/src/css/tldraw.css +++ b/src/css/tldraw.css @@ -26,5 +26,9 @@ html, } .tl-background { - background-color: transparent !important; + background-color: transparent; +} + +.tlui-debug-panel { + display: none; } diff --git a/src/physics/simulation.ts b/src/physics/simulation.ts index f6ec6b8..3829d2d 100644 --- a/src/physics/simulation.ts +++ b/src/physics/simulation.ts @@ -28,7 +28,7 @@ export class PhysicsWorld { public start() { this.world = new RAPIER.World(GRAVITY); - this.addShapes(this.editor.getSelectedShapes()); + this.addShapes(this.editor.getCurrentPageShapes()); const simLoop = () => { this.world.step(); diff --git a/src/physics/ui/PhysicsControls.tsx b/src/physics/ui/PhysicsControls.tsx index 477e264..e15fbb5 100644 --- a/src/physics/ui/PhysicsControls.tsx +++ b/src/physics/ui/PhysicsControls.tsx @@ -1,47 +1,17 @@ -import { track, useEditor } from "@tldraw/tldraw"; -import { useEffect, useState } from "react"; +import { TLUnknownShape, useEditor } from "@tldraw/tldraw"; +import { useEffect } from "react"; import { usePhysicsSimulation } from "../simulation"; -// import "../../css/physics-ui.css"; -export const SimControls = track(() => { +export const SimControls = ({ shapes }: { shapes: TLUnknownShape[] }) => { const editor = useEditor(); - const [physicsEnabled, setPhysics] = useState(false); useEffect(() => { - const togglePhysics = () => { - setPhysics(prev => !prev); - }; - - window.addEventListener('togglePhysicsEvent', togglePhysics); - - return () => { - window.removeEventListener('togglePhysicsEvent', togglePhysics); - }; + editor.createShapes(shapes) + return () => { editor.deleteShapes(editor.getCurrentPageShapes()) } }, []); - const { addShapes } = usePhysicsSimulation(editor, physicsEnabled); - return ( -
-
- - -
-
- ); -}); + const { addShapes } = usePhysicsSimulation(editor, true); + + return (<>); +}; From ac4b41dad38097f0d1bef8d7487b81c23bf5265e Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 23:18:48 -0700 Subject: [PATCH 05/13] Get HTML to render properly (this actually works?!?) --- src/App.tsx | 70 +++++++++++----------- src/HTMLShapeUtil.tsx | 32 ++++++++++ src/css/default.css | 65 --------------------- src/css/style.css | 133 ++++++++++++++++++++++++++++++++++++++++++ src/css/tldraw.css | 34 ----------- src/css/toggle.css | 19 ------ 6 files changed, 198 insertions(+), 155 deletions(-) create mode 100644 src/HTMLShapeUtil.tsx delete mode 100644 src/css/default.css create mode 100644 src/css/style.css delete mode 100644 src/css/tldraw.css delete mode 100644 src/css/toggle.css diff --git a/src/App.tsx b/src/App.tsx index cff4c4f..c625515 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,12 @@ -import { atom, createShapeId, createTLUser, setUserPreferences, StoreSnapshot, Tldraw, TLGeoShape, TLInstance, TLRecord, TLShape, TLUiComponents, TLUnknownShape, TLUserPreferences, track, useEditor } from "@tldraw/tldraw"; +import { createShapeId, Tldraw, TLGeoShape, TLShape, TLUiComponents } from "@tldraw/tldraw"; import "@tldraw/tldraw/tldraw.css"; +import "./css/style.css" import { SimControls } from "./physics/ui/PhysicsControls"; import { uiOverrides } from "./physics/ui/overrides"; import { Helmet, HelmetProvider } from "react-helmet-async"; -import React, { Suspense, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; +import { HTMLShapeUtil, HTMLShape } from "./HTMLShapeUtil"; ReactDOM.createRoot(document.getElementById("root")!).render(); @@ -53,58 +55,51 @@ function App() { // Function to gather elements info asynchronously async function gatherElementsInfo() { - const rootElement = document.getElementById('root'); + 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); - } - console.log(w) + // if (!['P', 'UL'].includes(child.tagName)) { + // w = measureElementTextWidth(child); + // } + // console.log(w) info.push({ tagName: child.tagName, - position: { x: rect.left, y: rect.top }, - dimensions: { width: w, height: rect.height }, + x: rect.left, + y: rect.top, + w: w, + h: rect.height, + html: child.outerHTML }); }; } - // Example usage - // const element = document.getElementById('yourElementId'); // Replace 'yourElementId' with the actual ID - // if (element) { - // console.log(`Text width: ${textWidth}px`); - // } - // console.log(info.length); - // console.log(info); - return info; } - const shapes: TLGeoShape[] = elementsInfo.map((element, index) => ({ + const shapes: HTMLShape[] = elementsInfo.map((element) => ({ id: createShapeId(), - type: 'geo', - x: element.position.x, - y: element.position.y, + type: 'html', + x: element.x, + y: element.y, props: { - geo: "rectangle", - w: element.dimensions.width, - h: element.dimensions.height, - fill: 'solid', - color: 'green' + w: element.w, + h: element.h, + html: element.html, } })) shapes.push({ id: createShapeId(), - type: 'geo', + type: 'html', x: 0, y: window.innerHeight, props: { - geo: "rectangle", w: window.innerWidth, h: 20, - fill: 'solid' + html: "FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" } }) @@ -120,10 +115,10 @@ function App() { function Default() { return ( - <> - +
+ {/* - + */}
Orion Reed
@@ -175,7 +170,7 @@ function Default() { 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 - +
); } @@ -183,12 +178,13 @@ function Canvas({ shapes }: { shapes: TLShape[] }) { return (
- + {/* - + */} @@ -199,9 +195,9 @@ function Canvas({ shapes }: { shapes: TLShape[] }) { function Toggle() { return ( <> - + {/* - + */} diff --git a/src/HTMLShapeUtil.tsx b/src/HTMLShapeUtil.tsx new file mode 100644 index 0000000..7503eb3 --- /dev/null +++ b/src/HTMLShapeUtil.tsx @@ -0,0 +1,32 @@ +import { Rectangle2d, TLBaseShape } 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 + + getDefaultProps(): HTMLShape['props'] { + return { + w: 100, + h: 100, + html: "
" + } + } + + getGeometry(shape: IHTMLShape) { + 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/css/default.css b/src/css/default.css deleted file mode 100644 index 7a6da37..0000000 --- a/src/css/default.css +++ /dev/null @@ -1,65 +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/css/style.css b/src/css/style.css new file mode 100644 index 0000000..392858f --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,133 @@ +@import url("reset.css"); + +html, +body { + padding: 0; + margin: 0; + overscroll-behavior: none; + touch-action: none; + min-height: 100vh; + min-height: -webkit-fill-available; + height: 100%; + /* font-family: "Inter", sans-serif; */ +} + +.tldraw__editor { + 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; +} +.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 { + position: absolute; + z-index: 99999; + top: 10px; + right: 10px; + width: 50px; + height: 50px; + background: none; + border: none; + cursor: pointer; + opacity: 0.25; + &:hover { + opacity: 1; + } + & img { + width: 100%; + height: 100%; + } +} + +@media (max-width: 600px) { + body { + 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; + } +} diff --git a/src/css/tldraw.css b/src/css/tldraw.css deleted file mode 100644 index b938a37..0000000 --- a/src/css/tldraw.css +++ /dev/null @@ -1,34 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); - -html, -body { - padding: 0; - margin: 0; - font-family: "Inter", sans-serif; - overscroll-behavior: none; - touch-action: none; - min-height: 100vh; - /* font-size: 16px; */ - /* mobile viewport bug fix */ - min-height: -webkit-fill-available; - height: 100%; -} - -html, -* { - box-sizing: border-box; -} - -.tldraw__editor { - position: fixed; - inset: 0px; - overflow: hidden; -} - -.tl-background { - background-color: transparent; -} - -.tlui-debug-panel { - display: none; -} diff --git a/src/css/toggle.css b/src/css/toggle.css deleted file mode 100644 index 94f0ab1..0000000 --- a/src/css/toggle.css +++ /dev/null @@ -1,19 +0,0 @@ -#toggle-physics { - position: absolute; - z-index: 99999; - top: 10px; - right: 10px; - width: 50px; - height: 50px; - background: none; - border: none; - cursor: pointer; - opacity: 0.25; - &:hover { - opacity: 1; - } - & img { - width: 100%; - height: 100%; - } -} From d2f317106903e6a0d705fb4c38f3269455a859f6 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 24 Mar 2024 23:48:40 -0700 Subject: [PATCH 06/13] Remove mobile action menu --- src/App.tsx | 17 ++++++++++++----- src/css/style.css | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c625515..5fd3a02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,15 +16,15 @@ const components: TLUiComponents = { PageMenu: null, NavigationPanel: null, DebugMenu: null, + ContextMenu: null, + ActionsMenu: null, + QuickActions: null, + MainMenu: null, MenuPanel: null, - // ContextMenu: null, - // ActionsMenu: null, // ZoomMenu: null, - // MainMenu: null, // Minimap: null, // Toolbar: null, // KeyboardShortcutsDialog: null, - // QuickActions: null, // HelperButtons: null, // SharePanel: null, // TopPanel: null, @@ -33,6 +33,7 @@ const components: TLUiComponents = { function App() { const [isPhysicsEnabled, setIsPhysicsEnabled] = useState(false); const [elementsInfo, setElementsInfo] = useState([]); + const [fadeClass, setFadeClass] = useState(''); // State to control the fade class useEffect(() => { const togglePhysics = async () => { @@ -40,9 +41,12 @@ function App() { const info = await gatherElementsInfo(); setElementsInfo(info); setIsPhysicsEnabled(true); // Enable physics only after gathering info + setFadeClass('fade-out'); // Start fading out the Default component + // setTimeout(() => setFadeClass('fade-in'), 500); // Wait for fade-out to complete before fading in Canvas } else { setIsPhysicsEnabled(false); setElementsInfo([]); // Reset elements info when disabling physics + setFadeClass(''); // Reset fade class to show Default component normally } }; @@ -107,7 +111,10 @@ function App() { - {isPhysicsEnabled && elementsInfo.length > 0 ? : } +
+ {} +
+ {isPhysicsEnabled && elementsInfo.length > 0 ? : null}
); diff --git a/src/css/style.css b/src/css/style.css index 392858f..57a84cb 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -4,7 +4,6 @@ html, body { padding: 0; margin: 0; - overscroll-behavior: none; touch-action: none; min-height: 100vh; min-height: -webkit-fill-available; @@ -13,6 +12,7 @@ body { } .tldraw__editor { + overscroll-behavior: none; position: fixed; inset: 0px; overflow: hidden; @@ -102,7 +102,7 @@ ul { } @media (max-width: 600px) { - body { + main { padding: 2em; } header { @@ -131,3 +131,16 @@ ul { font-size: 1.1rem; } } + +.fade-out { + opacity: 0 !important; + transition: opacity 1s ease-in-out; + /* visibility: hidden; */ + /* display: none; */ +} + +.fade-in { + opacity: 1; + transition: opacity 0.5s ease-in-out; + /* visibility: visible; */ +} From 366cba1ec18cf3678f5d64f572679ea1b6dad48a Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 25 Mar 2024 00:22:13 -0700 Subject: [PATCH 07/13] physics go brrrrrr --- package.json | 4 +- src/App.tsx | 209 ++---------------- .../causal-islands-integration-domain.pdf | 0 src/css/style.css | 2 +- src/physics/ui/overrides.ts | 22 -- src/ts/components/Canvas.tsx | 37 ++++ src/ts/components/Contact.tsx | 14 ++ src/ts/components/Default.tsx | 59 +++++ src/ts/components/Toggle.tsx | 11 + .../ui => ts/physics}/PhysicsControls.tsx | 4 +- src/{ => ts}/physics/config.ts | 0 src/{ => ts}/physics/math.ts | 0 src/{ => ts}/physics/simulation.ts | 16 +- src/{ => ts/shapes}/HTMLShapeUtil.tsx | 2 +- src/utils.tsx | 61 +++++ 15 files changed, 216 insertions(+), 225 deletions(-) rename src/{objects => artifacts}/causal-islands-integration-domain.pdf (100%) delete mode 100644 src/physics/ui/overrides.ts create mode 100644 src/ts/components/Canvas.tsx create mode 100644 src/ts/components/Contact.tsx create mode 100644 src/ts/components/Default.tsx create mode 100644 src/ts/components/Toggle.tsx rename src/{physics/ui => ts/physics}/PhysicsControls.tsx (72%) rename src/{ => ts}/physics/config.ts (100%) rename src/{ => ts}/physics/math.ts (100%) rename src/{ => ts}/physics/simulation.ts (97%) rename src/{ => ts/shapes}/HTMLShapeUtil.tsx (95%) create mode 100644 src/utils.tsx diff --git a/package.json b/package.json index a236846..6852859 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ "@dimforge/rapier2d": "latest", "@tldraw/tldraw": "2.0.2", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-helmet-async": "^2.0.4" + "react-dom": "^18.2.0" }, "devDependencies": { "@biomejs/biome": "1.4.1", @@ -25,7 +24,6 @@ "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", "concurrently": "^8.2.0", - "gh-pages": "^6.1.1", "typescript": "^5.0.2", "vite": "^4.4.5", "vite-plugin-top-level-await": "^1.3.1", diff --git a/src/App.tsx b/src/App.tsx index 5fd3a02..f7d0bae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,16 @@ -import { createShapeId, Tldraw, TLGeoShape, TLShape, TLUiComponents } from "@tldraw/tldraw"; +import { createShapeId, TLUiComponents } from "@tldraw/tldraw"; import "@tldraw/tldraw/tldraw.css"; import "./css/style.css" -import { SimControls } from "./physics/ui/PhysicsControls"; -import { uiOverrides } from "./physics/ui/overrides"; -import { Helmet, HelmetProvider } from "react-helmet-async"; import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; -import { HTMLShapeUtil, HTMLShape } from "./HTMLShapeUtil"; +import { HTMLShape } from "./ts/shapes/HTMLShapeUtil"; +import { Default } from "./ts/components/Default"; +import { Canvas } from "./ts/components/Canvas"; +import { Toggle } from "./ts/components/Toggle"; +import { gatherElementsInfo } from "./utils"; ReactDOM.createRoot(document.getElementById("root")!).render(); -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, -} - function App() { const [isPhysicsEnabled, setIsPhysicsEnabled] = useState(false); const [elementsInfo, setElementsInfo] = useState([]); @@ -57,31 +38,7 @@ function App() { }; }, [isPhysicsEnabled]); - // Function to gather elements info asynchronously - 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); - // } - // console.log(w) - info.push({ - tagName: child.tagName, - x: rect.left, - y: rect.top, - w: w, - h: rect.height, - html: child.outerHTML - }); - }; - } - return info; - } + const shapes: HTMLShape[] = elementsInfo.map((element) => ({ id: createShapeId(), @@ -97,157 +54,29 @@ function App() { shapes.push({ id: createShapeId(), - type: 'html', + type: 'geo', x: 0, y: window.innerHeight, props: { w: window.innerWidth, - h: 20, - html: "FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" + h: 50, + color: 'grey', + fill: 'solid' + }, + meta: { + fixed: true } }) return ( - - -
- {} -
- {isPhysicsEnabled && elementsInfo.length > 0 ? : null} -
+ +
+ {} +
+ {isPhysicsEnabled && elementsInfo.length > 0 ? : null}
); }; -function Default() { - return ( -
- {/* - - */} -
- Orion Reed -
-

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

- -
- ); -} - -function Canvas({ shapes }: { shapes: TLShape[] }) { - - return ( -
- {/* - - */} - - - -
- ); -} - -function Toggle() { - return ( - <> - {/* - - */} - - - ); -} - -function Contact() { - return ( -
-

Contact

-

Twitter: @OrionReedOne

-

Mastodon: orion@hci.social

-

Email: me@orionreed.com

-

GitHub: OrionReed

-
- - ) -} - -function measureElementTextWidth(element: Element) { - // 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/objects/causal-islands-integration-domain.pdf b/src/artifacts/causal-islands-integration-domain.pdf similarity index 100% rename from src/objects/causal-islands-integration-domain.pdf rename to src/artifacts/causal-islands-integration-domain.pdf diff --git a/src/css/style.css b/src/css/style.css index 57a84cb..8bf3b75 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -134,7 +134,7 @@ ul { .fade-out { opacity: 0 !important; - transition: opacity 1s ease-in-out; + transition: opacity 0.2s ease-in-out; /* visibility: hidden; */ /* display: none; */ } diff --git a/src/physics/ui/overrides.ts b/src/physics/ui/overrides.ts deleted file mode 100644 index ae905e7..0000000 --- a/src/physics/ui/overrides.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - TLUiEventSource, - TLUiOverrides, - TLUiTranslationKey, -} from "@tldraw/tldraw"; - -// In order to see select our custom shape tool, we need to add it to the ui. -export const uiOverrides: TLUiOverrides = { - actions(_editor, actions) { - actions['toggle-physics'] = { - id: 'toggle-physics', - label: 'Toggle Physics' as TLUiTranslationKey, - readonlyOk: true, - kbd: 'p', - onSelect(_source: TLUiEventSource) { - const event = new CustomEvent('togglePhysicsEvent'); - window.dispatchEvent(event); - }, - } - return actions - }, -} \ No newline at end of file diff --git a/src/ts/components/Canvas.tsx b/src/ts/components/Canvas.tsx new file mode 100644 index 0000000..e4c5c80 --- /dev/null +++ b/src/ts/components/Canvas.tsx @@ -0,0 +1,37 @@ +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 ( +
+ + + +
+ ); +} diff --git a/src/ts/components/Contact.tsx b/src/ts/components/Contact.tsx new file mode 100644 index 0000000..5838f93 --- /dev/null +++ b/src/ts/components/Contact.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +function Contact() { + return ( +
+

Contact

+

Twitter: @OrionReedOne

+

Mastodon: orion@hci.social

+

Email: me@orionreed.com

+

GitHub: OrionReed

+
+ + ); +} diff --git a/src/ts/components/Default.tsx b/src/ts/components/Default.tsx new file mode 100644 index 0000000..f0f16c2 --- /dev/null +++ b/src/ts/components/Default.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +export function Default() { + return ( +
+
+ Orion Reed +
+

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/ts/components/Toggle.tsx b/src/ts/components/Toggle.tsx new file mode 100644 index 0000000..d7614f8 --- /dev/null +++ b/src/ts/components/Toggle.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export function Toggle() { + return ( + <> + + + ); +} diff --git a/src/physics/ui/PhysicsControls.tsx b/src/ts/physics/PhysicsControls.tsx similarity index 72% rename from src/physics/ui/PhysicsControls.tsx rename to src/ts/physics/PhysicsControls.tsx index e15fbb5..444f4a0 100644 --- a/src/physics/ui/PhysicsControls.tsx +++ b/src/ts/physics/PhysicsControls.tsx @@ -1,8 +1,8 @@ import { TLUnknownShape, useEditor } from "@tldraw/tldraw"; import { useEffect } from "react"; -import { usePhysicsSimulation } from "../simulation"; +import { usePhysicsSimulation } from "./simulation"; -export const SimControls = ({ shapes }: { shapes: TLUnknownShape[] }) => { +export const SimController = ({ shapes }: { shapes: TLUnknownShape[] }) => { const editor = useEditor(); useEffect(() => { diff --git a/src/physics/config.ts b/src/ts/physics/config.ts similarity index 100% rename from src/physics/config.ts rename to src/ts/physics/config.ts diff --git a/src/physics/math.ts b/src/ts/physics/math.ts similarity index 100% rename from src/physics/math.ts rename to src/ts/physics/math.ts diff --git a/src/physics/simulation.ts b/src/ts/physics/simulation.ts similarity index 97% rename from src/physics/simulation.ts rename to src/ts/physics/simulation.ts index 3829d2d..f73900f 100644 --- a/src/physics/simulation.ts +++ b/src/ts/physics/simulation.ts @@ -9,6 +9,8 @@ type BodyWithShapeData = RAPIER.RigidBody & { }; type RigidbodyLookup = { [key: TLShapeId]: RAPIER.RigidBody }; +const START_DELAY = 1500; + export class PhysicsWorld { private editor: Editor; private world: RAPIER.World; @@ -28,6 +30,7 @@ export class PhysicsWorld { public start() { this.world = new RAPIER.World(GRAVITY); + // setTimeout(() => { this.addShapes(this.editor.getCurrentPageShapes()); const simLoop = () => { @@ -38,6 +41,7 @@ export class PhysicsWorld { }; simLoop(); return () => cancelAnimationFrame(this.animFrame); + // }, START_DELAY); }; public stop() { @@ -55,6 +59,7 @@ export class PhysicsWorld { } switch (shape.type) { + case "html": case "geo": case "image": case "video": @@ -72,12 +77,12 @@ export class PhysicsWorld { } createShape(shape: TLGeoShape | TLDrawShape) { - if (shape.props.dash === "dashed") return; // Skip dashed shapes - if (isRigidbody(shape.props.color)) { - const gravity = getGravityFromColor(shape.props.color) - const rb = this.createRigidbody(shape, gravity); + console.log('creating shape'); + if (!shape.meta.fixed) { + const rb = this.createRigidbody(shape, 1); this.createCollider(shape, rb); - } else { + } + else { this.createCollider(shape); } } @@ -379,7 +384,6 @@ export function usePhysicsSimulation(editor: Editor, enabled: boolean) { useEffect(() => { if (enabled) { sim.current.start(); - editor.selectNone(); return () => sim.current.stop(); } }, [enabled, sim]); diff --git a/src/HTMLShapeUtil.tsx b/src/ts/shapes/HTMLShapeUtil.tsx similarity index 95% rename from src/HTMLShapeUtil.tsx rename to src/ts/shapes/HTMLShapeUtil.tsx index 7503eb3..156b261 100644 --- a/src/HTMLShapeUtil.tsx +++ b/src/ts/shapes/HTMLShapeUtil.tsx @@ -23,7 +23,7 @@ export class HTMLShapeUtil extends ShapeUtil { } component(shape: HTMLShape) { - return
+ return
} indicator(shape: HTMLShape) { diff --git a/src/utils.tsx b/src/utils.tsx new file mode 100644 index 0000000..4ab56b6 --- /dev/null +++ b/src/utils.tsx @@ -0,0 +1,61 @@ +export 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; +} + + +// Function to gather elements info asynchronously +export 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); + } + // 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; +} From 3423aeeeb66d0695bb9bda1424a0bf7a591f1d61 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 25 Mar 2024 00:29:01 -0700 Subject: [PATCH 08/13] html is resizeable --- src/ts/physics/PhysicsControls.tsx | 2 +- src/ts/shapes/HTMLShapeUtil.tsx | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ts/physics/PhysicsControls.tsx b/src/ts/physics/PhysicsControls.tsx index 444f4a0..2b7c118 100644 --- a/src/ts/physics/PhysicsControls.tsx +++ b/src/ts/physics/PhysicsControls.tsx @@ -11,7 +11,7 @@ export const SimController = ({ shapes }: { shapes: TLUnknownShape[] }) => { }, []); - const { addShapes } = usePhysicsSimulation(editor, true); + const { addShapes } = usePhysicsSimulation(editor, false); return (<>); }; diff --git a/src/ts/shapes/HTMLShapeUtil.tsx b/src/ts/shapes/HTMLShapeUtil.tsx index 156b261..d89a892 100644 --- a/src/ts/shapes/HTMLShapeUtil.tsx +++ b/src/ts/shapes/HTMLShapeUtil.tsx @@ -1,10 +1,14 @@ -import { Rectangle2d, TLBaseShape } from '@tldraw/tldraw'; +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 { @@ -14,6 +18,10 @@ export class HTMLShapeUtil extends ShapeUtil { } } + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } + getGeometry(shape: IHTMLShape) { return new Rectangle2d({ width: shape.props.w, @@ -24,6 +32,7 @@ export class HTMLShapeUtil extends ShapeUtil { component(shape: HTMLShape) { return
+ } indicator(shape: HTMLShape) { From a9e29ae196998db99138b6fc7ad1fbb20d703712 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 25 Mar 2024 00:33:23 -0700 Subject: [PATCH 09/13] physics on by default --- src/ts/physics/PhysicsControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ts/physics/PhysicsControls.tsx b/src/ts/physics/PhysicsControls.tsx index 2b7c118..444f4a0 100644 --- a/src/ts/physics/PhysicsControls.tsx +++ b/src/ts/physics/PhysicsControls.tsx @@ -11,7 +11,7 @@ export const SimController = ({ shapes }: { shapes: TLUnknownShape[] }) => { }, []); - const { addShapes } = usePhysicsSimulation(editor, false); + const { addShapes } = usePhysicsSimulation(editor, true); return (<>); }; From e595cc1135a177bc49d12c669730ead34b85cac4 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 25 Mar 2024 01:13:53 -0700 Subject: [PATCH 10/13] some fixes --- package.json | 6 +- src/App.tsx | 81 ++++----------- src/css/style.css | 8 ++ .../causal-islands-integration-domain.pdf | 0 src/ts/components/Contact.tsx | 13 ++- src/ts/components/Default.tsx | 10 +- src/ts/components/Toggle.tsx | 2 - src/ts/hooks/usePhysics.ts | 99 +++++++++++++++++++ src/ts/shapes/HTMLShapeUtil.tsx | 2 +- src/utils.tsx | 88 ++++++----------- 10 files changed, 173 insertions(+), 136 deletions(-) rename src/{artifacts => public/artifact}/causal-islands-integration-domain.pdf (100%) create mode 100644 src/ts/hooks/usePhysics.ts diff --git a/package.json b/package.json index 6852859..1c92dce 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@dimforge/rapier2d": "latest", "@tldraw/tldraw": "2.0.2", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3" }, "devDependencies": { "@biomejs/biome": "1.4.1", @@ -23,10 +24,9 @@ "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", - "concurrently": "^8.2.0", "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 index f7d0bae..f7e80a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,76 +7,35 @@ import { HTMLShape } from "./ts/shapes/HTMLShapeUtil"; import { Default } from "./ts/components/Default"; import { Canvas } from "./ts/components/Canvas"; import { Toggle } from "./ts/components/Toggle"; -import { gatherElementsInfo } from "./utils"; +import { usePhysics } from "./ts/hooks/usePhysics.ts" +import { createShapes } from "./utils"; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { Contact } from "./ts/components/Contact.tsx"; ReactDOM.createRoot(document.getElementById("root")!).render(); function App() { - const [isPhysicsEnabled, setIsPhysicsEnabled] = useState(false); - const [elementsInfo, setElementsInfo] = useState([]); - const [fadeClass, setFadeClass] = useState(''); // State to control the fade class - useEffect(() => { - const togglePhysics = async () => { - if (!isPhysicsEnabled) { - const info = await gatherElementsInfo(); - setElementsInfo(info); - setIsPhysicsEnabled(true); // Enable physics only after gathering info - setFadeClass('fade-out'); // Start fading out the Default component - // setTimeout(() => setFadeClass('fade-in'), 500); // Wait for fade-out to complete before fading in Canvas - } else { - setIsPhysicsEnabled(false); - setElementsInfo([]); // Reset elements info when disabling physics - setFadeClass(''); // Reset fade class to show Default component normally - } - }; - - window.addEventListener('togglePhysicsEvent', togglePhysics); - - return () => { - window.removeEventListener('togglePhysicsEvent', togglePhysics); - }; - }, [isPhysicsEnabled]); - - - - const shapes: HTMLShape[] = elementsInfo.map((element) => ({ - id: createShapeId(), - type: 'html', - x: element.x, - y: element.y, - props: { - w: element.w, - h: element.h, - html: element.html, - } - })) - - shapes.push({ - id: createShapeId(), - type: 'geo', - x: 0, - y: window.innerHeight, - props: { - w: window.innerWidth, - h: 50, - color: 'grey', - fill: 'solid' - }, - meta: { - fixed: true - } - }) return ( - -
- {} -
- {isPhysicsEnabled && elementsInfo.length > 0 ? : null} + + + } /> + } /> + +
); }; - +function Home() { + const { isPhysicsEnabled, elementsInfo, fadeClass } = usePhysics(); + const shapes = createShapes(elementsInfo) + return ( + <> +
+ {} +
+ {isPhysicsEnabled && elementsInfo.length > 0 ? : null}) +} \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css index 8bf3b75..c2f7b2a 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -64,6 +64,14 @@ p { font-size: 1.1em; font-variation-settings: "wght" 350; } + +a { + text-decoration: none; + &:hover { + text-decoration: underline; + } +} + .dinkus { display: block; text-align: center; diff --git a/src/artifacts/causal-islands-integration-domain.pdf b/src/public/artifact/causal-islands-integration-domain.pdf similarity index 100% rename from src/artifacts/causal-islands-integration-domain.pdf rename to src/public/artifact/causal-islands-integration-domain.pdf diff --git a/src/ts/components/Contact.tsx b/src/ts/components/Contact.tsx index 5838f93..f592cd2 100644 --- a/src/ts/components/Contact.tsx +++ b/src/ts/components/Contact.tsx @@ -1,14 +1,17 @@ -import React from "react"; - -function Contact() { +export function Contact() { return ( -
+
+
+ + Orion Reed + +

Contact

Twitter: @OrionReedOne

Mastodon: orion@hci.social

Email: me@orionreed.com

GitHub: OrionReed

-
+
); } diff --git a/src/ts/components/Default.tsx b/src/ts/components/Default.tsx index f0f16c2..3fe1071 100644 --- a/src/ts/components/Default.tsx +++ b/src/ts/components/Default.tsx @@ -1,5 +1,3 @@ -import React from "react"; - export function Default() { return (
@@ -24,9 +22,9 @@ export function Default() {

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 + 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.

@@ -42,7 +40,7 @@ export function Default() {

Talks