"use client" import type React from "react" import { useEffect, useRef, useState } from "react" import Link from "next/link" // Inline Propagator class (simplified from @folkjs/propagators) type PropagatorFunction = (source: EventTarget, target: EventTarget, event: Event) => any interface PropagatorOptions { source?: EventTarget | null target?: EventTarget | null event?: string | null handler?: PropagatorFunction | null } class Propagator { private source: EventTarget | null = null private target: EventTarget | null = null private eventName: string | null = null private handler: PropagatorFunction | null = null constructor(options: PropagatorOptions = {}) { const { source = null, target = null, event = null, handler = null } = options this.source = source this.target = target this.eventName = event this.handler = handler // Add listener if we have all necessary parts if (this.source && this.eventName) { this.source.addEventListener(this.eventName, this.handleEvent) } } private handleEvent = (event: Event) => { if (!this.source || !this.target || !this.handler) return try { this.handler(this.source, this.target, event) } catch (error) { console.error("Error in propagator handler:", error) } } propagate(event?: Event): void { if (!event && this.eventName) { event = new Event(this.eventName) } if (!event) return this.handleEvent(event) } dispose(): void { if (this.source && this.eventName) { this.source.removeEventListener(this.eventName, this.handleEvent) } this.source = null this.target = null this.handler = null } } type Tool = "select" | "draw" | "erase" | "rectangle" | "text" | "arrow" // Helper function to calculate distance from point to line segment function pointToLineDistance(px: number, py: number, x1: number, y1: number, x2: number, y2: number): number { const A = px - x1 const B = py - y1 const C = x2 - x1 const D = y2 - y1 const dot = A * C + B * D const lenSq = C * C + D * D let param = -1 if (lenSq !== 0) { param = dot / lenSq } let xx, yy if (param < 0) { xx = x1 yy = y1 } else if (param > 1) { xx = x2 yy = y2 } else { xx = x1 + param * C yy = y1 + param * D } const dx = px - xx const dy = py - yy return Math.sqrt(dx * dx + dy * dy) } // Constants const HIT_TOLERANCE = 10 // pixels - for line/arrow hit detection interface Shape { id: string type: "rectangle" | "ellipse" | "line" | "text" | "arrow" x: number y: number width?: number height?: number x2?: number y2?: number text?: string color: string // For arrows that connect shapes sourceShapeId?: string targetShapeId?: string // Propagator expression for live arrows expression?: string // Data value for shapes (used in propagation) value?: number } export default function ItalismPage() { const canvasRef = useRef(null) const [tool, setTool] = useState("select") const [shapes, setShapes] = useState([ { id: "1", type: "rectangle", x: 100, y: 100, width: 750, height: 50, color: "#6366f1", text: "Digital Liberation", }, { id: "2", type: "rectangle", x: 360, y: 180, width: 480, height: 50, color: "#6366f1", text: "Post-Appitalism" }, { id: "3", type: "rectangle", x: 50, y: 340, width: 1110, height: 50, color: "#10b981", text: "Collaborative Economy", }, { id: "4", type: "ellipse", x: 270, y: 430, width: 1020, height: 40, color: "#10b981", text: "Decentralized" }, { id: "5", type: "ellipse", x: 310, y: 530, width: 1110, height: 40, color: "#10b981", text: "Future" }, { id: "6", type: "rectangle", x: 80, y: 605, width: 1110, height: 50, color: "#6366f1", text: "Community" }, { id: "7", type: "rectangle", x: 290, y: 710, width: 630, height: 50, color: "#8b5cf6", text: "Innovation" }, ]) const [isDrawing, setIsDrawing] = useState(false) const [currentShape, setCurrentShape] = useState | null>(null) const [selectedShape, setSelectedShape] = useState(null) const [isFullscreen, setIsFullscreen] = useState(false) const [isDragging, setIsDragging] = useState(false) const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) const [arrowStartShape, setArrowStartShape] = useState(null) const [propagators, setPropagators] = useState>(new Map()) const [editingArrow, setEditingArrow] = useState(null) const [eventTargets, setEventTargets] = useState>(new Map()) // Undo/Redo state - using useRef to avoid stale closure issues const historyRef = useRef([]) const historyIndexRef = useRef(-1) const [, forceUpdate] = useState({}) const isInitialized = useRef(false) // Initialize history with current shapes on mount useEffect(() => { if (!isInitialized.current) { historyRef.current = [JSON.parse(JSON.stringify(shapes))] historyIndexRef.current = 0 isInitialized.current = true } }, []) // Save state to history (called after any shape modification) const saveToHistory = (newShapes: Shape[]) => { if (!isInitialized.current) return // Truncate history after current index (discard redo states) historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1) // Add new state (deep clone to prevent reference issues) historyRef.current.push(JSON.parse(JSON.stringify(newShapes))) // Limit to 50 states to prevent memory issues if (historyRef.current.length > 50) { historyRef.current.shift() } else { historyIndexRef.current++ } setShapes(newShapes) } // Undo function - go back one state const undo = () => { if (historyIndexRef.current > 0) { historyIndexRef.current-- const previousState = historyRef.current[historyIndexRef.current] setShapes(previousState) forceUpdate({}) // Force re-render to update button states } } // Redo function - go forward one state const redo = () => { if (historyIndexRef.current < historyRef.current.length - 1) { historyIndexRef.current++ const nextState = historyRef.current[historyIndexRef.current] setShapes(nextState) forceUpdate({}) // Force re-render to update button states } } // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ctrl+Z or Cmd+Z for undo if ((e.ctrlKey || e.metaKey) && e.code === 'KeyZ' && !e.shiftKey) { e.preventDefault() undo() } // Ctrl+Shift+Z or Cmd+Shift+Z for redo else if ((e.ctrlKey || e.metaKey) && e.code === 'KeyZ' && e.shiftKey) { e.preventDefault() redo() } // Delete key to delete selected shape else if (e.key === 'Delete' && selectedShape) { e.preventDefault() const clicked = shapes.find(s => s.id === selectedShape) if (clicked) { // Cleanup propagator if deleting an arrow if (clicked.type === "arrow") { const propagator = propagators.get(clicked.id) if (propagator) { propagator.dispose() setPropagators((prev) => { const next = new Map(prev) next.delete(clicked.id) return next }) } setEventTargets((prev) => { const next = new Map(prev) next.delete(clicked.id) return next }) } const newShapes = shapes.filter((shape) => shape.id !== clicked.id) saveToHistory(newShapes) setSelectedShape(null) } } // Escape to deselect else if (e.key === 'Escape') { e.preventDefault() setSelectedShape(null) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [selectedShape, shapes, propagators]) // Helper function to get the center of a shape const getShapeCenter = (shape: Shape): { x: number; y: number } => { if (shape.width && shape.height) { return { x: shape.x + shape.width / 2, y: shape.y + shape.height / 2, } } return { x: shape.x, y: shape.y } } // Helper function to find shape at coordinates (excludes arrows/lines - used for arrow tool) const findShapeAt = (x: number, y: number): Shape | null => { return ( shapes.find((shape) => { if (shape.type === "arrow" || shape.type === "line") return false if (shape.width && shape.height) { return x >= shape.x && x <= shape.x + shape.width && y >= shape.y && y <= shape.y + shape.height } else if (shape.type === "text" && shape.text) { const textWidth = shape.text.length * 12 const textHeight = 20 return x >= shape.x && x <= shape.x + textWidth && y >= shape.y - textHeight && y <= shape.y } return false }) || null ) } // Helper function to check if a point is inside/near a shape (includes all shape types) const isPointInShape = (x: number, y: number, shape: Shape): boolean => { if (shape.width && shape.height) { return x >= shape.x && x <= shape.x + shape.width && y >= shape.y && y <= shape.y + shape.height } else if (shape.type === "text" && shape.text) { const textWidth = shape.text.length * 12 const textHeight = 20 return x >= shape.x && x <= shape.x + textWidth && y >= shape.y - textHeight && y <= shape.y } else if ((shape.type === "line" || shape.type === "arrow") && shape.x2 && shape.y2) { const distance = pointToLineDistance(x, y, shape.x, shape.y, shape.x2, shape.y2) return distance < HIT_TOLERANCE } return false } // Create a simple propagator for an arrow const createPropagatorForArrow = (arrow: Shape) => { if (!arrow.sourceShapeId || !arrow.targetShapeId) return const sourceShape = shapes.find((s) => s.id === arrow.sourceShapeId) const targetShape = shapes.find((s) => s.id === arrow.targetShapeId) if (!sourceShape || !targetShape) return // Create EventTargets for the connection const mockSource = new EventTarget() const mockTarget = new EventTarget() // Store shape IDs on EventTargets so handler can reference them ;(mockSource as any)._shapeId = arrow.sourceShapeId ;(mockTarget as any)._shapeId = arrow.targetShapeId const expression = arrow.expression || "value: from.value" try { const propagator = new Propagator({ source: mockSource, target: mockTarget, event: "update", handler: (from: any, to: any) => { // Use setShapes with function to get CURRENT state (avoid stale closure) setShapes((currentShapes) => { const currentSourceShape = currentShapes.find((s) => s.id === (from as any)._shapeId) if (!currentSourceShape || currentSourceShape.value === undefined) { console.log("⚠️ No source value to propagate") return currentShapes // return unchanged } const sourceValue = currentSourceShape.value const targetId = (to as any)._shapeId console.log(`✅ Propagating from ${(from as any)._shapeId} to ${targetId}: ${sourceValue}`) // Update target shape with value return currentShapes.map((s) => s.id === targetId ? { ...s, value: sourceValue } : s ) }) }, }) // Store both propagator and EventTargets in their respective Maps setPropagators((prev) => { const next = new Map(prev) next.set(arrow.id, propagator) return next }) setEventTargets((prev) => { const next = new Map(prev) next.set(arrow.id, { source: mockSource, target: mockTarget }) return next }) } catch (error) { console.error("Failed to create propagator:", error) } } useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") if (!ctx) return // Set canvas size canvas.width = canvas.offsetWidth canvas.height = canvas.offsetHeight // Clear canvas ctx.fillStyle = "#0f172a" ctx.fillRect(0, 0, canvas.width, canvas.height) // Helper function to draw arrowhead const drawArrowhead = (x1: number, y1: number, x2: number, y2: number, color: string) => { const headLength = 15 const angle = Math.atan2(y2 - y1, x2 - x1) ctx.fillStyle = color ctx.beginPath() ctx.moveTo(x2, y2) ctx.lineTo( x2 - headLength * Math.cos(angle - Math.PI / 6), y2 - headLength * Math.sin(angle - Math.PI / 6) ) ctx.lineTo( x2 - headLength * Math.cos(angle + Math.PI / 6), y2 - headLength * Math.sin(angle + Math.PI / 6) ) ctx.closePath() ctx.fill() } // Draw shapes shapes.forEach((shape) => { ctx.strokeStyle = shape.color ctx.lineWidth = 2 ctx.fillStyle = shape.color ctx.font = "16px sans-serif" if (shape.type === "rectangle" && shape.width && shape.height) { ctx.strokeRect(shape.x, shape.y, shape.width, shape.height) if (shape.text) { ctx.fillText(shape.text, shape.x + 10, shape.y + shape.height / 2 + 5) } } else if (shape.type === "ellipse" && shape.width && shape.height) { ctx.beginPath() ctx.ellipse( shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width / 2, shape.height / 2, 0, 0, 2 * Math.PI, ) ctx.stroke() if (shape.text) { ctx.fillText(shape.text, shape.x + shape.width / 2 - 50, shape.y + shape.height / 2 + 5) } } else if (shape.type === "line" && shape.x2 && shape.y2) { ctx.beginPath() ctx.moveTo(shape.x, shape.y) ctx.lineTo(shape.x2, shape.y2) ctx.stroke() } else if (shape.type === "arrow" && shape.x2 && shape.y2) { // Draw arrow line with highlight if selected const isSelected = selectedShape === shape.id ctx.strokeStyle = isSelected ? "#22d3ee" : shape.color ctx.lineWidth = isSelected ? 4 : 2 ctx.beginPath() ctx.moveTo(shape.x, shape.y) ctx.lineTo(shape.x2, shape.y2) ctx.stroke() // Draw arrowhead drawArrowhead(shape.x, shape.y, shape.x2, shape.y2, isSelected ? "#22d3ee" : shape.color) // Reset line width ctx.lineWidth = 2 } else if (shape.type === "text" && shape.text) { ctx.font = "20px sans-serif" ctx.fillStyle = shape.color ctx.fillText(shape.text, shape.x, shape.y) } // Highlight selected shape if (selectedShape === shape.id && shape.width && shape.height) { ctx.strokeStyle = "#22d3ee" ctx.lineWidth = 3 ctx.strokeRect(shape.x - 5, shape.y - 5, shape.width + 10, shape.height + 10) } // Show connection point (center dot) for shapes that can be connected if (shape.type !== "arrow" && shape.type !== "line" && (shape.width || shape.height)) { const center = getShapeCenter(shape) ctx.fillStyle = "#22d3ee" ctx.beginPath() ctx.arc(center.x, center.y, 3, 0, 2 * Math.PI) ctx.fill() } // Show value label if shape has a value if (shape.value !== undefined && shape.type !== "arrow") { ctx.fillStyle = "#fbbf24" ctx.font = "bold 12px sans-serif" ctx.fillText(`${shape.value}`, shape.x + 5, shape.y - 5) } }) // Draw current shape being drawn if (currentShape && isDrawing) { ctx.strokeStyle = currentShape.color || "#6366f1" ctx.lineWidth = 2 ctx.fillStyle = currentShape.color || "#6366f1" ctx.setLineDash([5, 5]) // Dashed line for preview if (currentShape.type === "rectangle" && currentShape.width && currentShape.height) { ctx.strokeRect(currentShape.x || 0, currentShape.y || 0, currentShape.width, currentShape.height) } else if (currentShape.type === "line" && currentShape.x2 && currentShape.y2) { ctx.beginPath() ctx.moveTo(currentShape.x || 0, currentShape.y || 0) ctx.lineTo(currentShape.x2, currentShape.y2) ctx.stroke() } else if (currentShape.type === "arrow" && currentShape.x2 && currentShape.y2) { ctx.beginPath() ctx.moveTo(currentShape.x || 0, currentShape.y || 0) ctx.lineTo(currentShape.x2, currentShape.y2) ctx.stroke() drawArrowhead(currentShape.x || 0, currentShape.y || 0, currentShape.x2, currentShape.y2, currentShape.color || "#6366f1") } ctx.setLineDash([]) // Reset to solid line } }, [shapes, selectedShape, currentShape, isDrawing]) const handleMouseDown = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top if (tool === "select") { // Find clicked shape (including arrows) const clicked = shapes.find((shape) => isPointInShape(x, y, shape)) setSelectedShape(clicked?.id || null) // If a shape was clicked, prepare for dragging (but not for arrows - they're connections) if (clicked && clicked.type !== "arrow" && clicked.type !== "line") { setIsDragging(true) setDragOffset({ x: x - clicked.x, y: y - clicked.y, }) } } else if (tool === "erase") { // Find and delete clicked shape const clicked = shapes.find((shape) => isPointInShape(x, y, shape)) if (clicked) { // Cleanup propagator if deleting an arrow if (clicked.type === "arrow") { const propagator = propagators.get(clicked.id) if (propagator) { propagator.dispose() setPropagators((prev) => { const next = new Map(prev) next.delete(clicked.id) return next }) } setEventTargets((prev) => { const next = new Map(prev) next.delete(clicked.id) return next }) } const newShapes = shapes.filter((shape) => shape.id !== clicked.id) saveToHistory(newShapes) setSelectedShape(null) } } else if (tool === "text") { // Prompt for text input const text = prompt("Enter text:") if (text) { const newShape: Shape = { id: Date.now().toString(), type: "text", x, y, text, color: "#6366f1", } saveToHistory([...shapes, newShape]) } } else if (tool === "arrow") { // Special handling for arrow tool - snap to shapes const shapeAtClick = findShapeAt(x, y) if (shapeAtClick) { // Clicked on a shape - start arrow from its center setArrowStartShape(shapeAtClick.id) const center = getShapeCenter(shapeAtClick) setIsDrawing(true) setCurrentShape({ id: Date.now().toString(), type: "arrow", x: center.x, y: center.y, color: "#6366f1", sourceShapeId: shapeAtClick.id, }) } } else if (tool === "draw" || tool === "rectangle") { setIsDrawing(true) setCurrentShape({ id: Date.now().toString(), type: tool === "rectangle" ? "rectangle" : "line", x, y, color: "#6366f1", }) } } const handleMouseMove = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Handle dragging selected shape if (isDragging && selectedShape && tool === "select") { setShapes( shapes.map((shape) => { if (shape.id === selectedShape) { const newX = x - dragOffset.x const newY = y - dragOffset.y // For lines and arrows, also update the end points if ((shape.type === "line" || shape.type === "arrow") && shape.x2 !== undefined && shape.y2 !== undefined) { const dx = newX - shape.x const dy = newY - shape.y return { ...shape, x: newX, y: newY, x2: shape.x2 + dx, y2: shape.y2 + dy, } } return { ...shape, x: newX, y: newY, } } return shape }) ) return } // Handle drawing new shapes if (isDrawing && currentShape) { if (currentShape.type === "rectangle") { setCurrentShape({ ...currentShape, width: x - (currentShape.x || 0), height: y - (currentShape.y || 0), }) } else if (currentShape.type === "arrow") { // For arrows, snap to target shape center if hovering over one const shapeAtMouse = findShapeAt(x, y) if (shapeAtMouse && shapeAtMouse.id !== arrowStartShape) { const center = getShapeCenter(shapeAtMouse) setCurrentShape({ ...currentShape, x2: center.x, y2: center.y, targetShapeId: shapeAtMouse.id, }) } else { setCurrentShape({ ...currentShape, x2: x, y2: y, targetShapeId: undefined, }) } } else if (currentShape.type === "line") { setCurrentShape({ ...currentShape, x2: x, y2: y, }) } } } const handleMouseUp = () => { if (isDrawing && currentShape) { let newShape = currentShape as Shape // Normalize rectangles with negative dimensions (drawn upward/leftward) if (newShape.type === "rectangle" && newShape.width !== undefined && newShape.height !== undefined) { if (newShape.width < 0) { newShape.x = newShape.x + newShape.width newShape.width = Math.abs(newShape.width) } if (newShape.height < 0) { newShape.y = newShape.y + newShape.height newShape.height = Math.abs(newShape.height) } } // If it's an arrow with both source and target, create a propagator const newShapesArray = [...shapes, newShape] if (newShape.type === "arrow" && newShape.sourceShapeId && newShape.targetShapeId) { newShape.expression = "value: from.value" // Default expression saveToHistory(newShapesArray) // Create propagator for this arrow setTimeout(() => createPropagatorForArrow(newShape), 0) } else { saveToHistory(newShapesArray) } setCurrentShape(null) } else if (isDragging) { // Save to history when dragging stops saveToHistory(shapes) } setIsDrawing(false) setIsDragging(false) setArrowStartShape(null) } const toggleFullscreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen() setIsFullscreen(true) } else { document.exitFullscreen() setIsFullscreen(false) } } return (
{/* Header */}

Interactive Canvas

← Back to Home
{/* Main Content */}
{/* Canvas */}
{currentShape && (
Drawing {currentShape.type}...
)}
{/* Sidebar */}

FolkJS Canvas

  • • Use toolbar to draw and create shapes
  • • Click and drag to move elements
  • • Double-click text to edit
  • • Use select tool to interact
  • • Press Space for{" "} fullscreen
{/* Toolbar */}

Tools

{[ { id: "select", label: "Select" }, { id: "draw", label: "Draw" }, { id: "erase", label: "Erase" }, { id: "rectangle", label: "Rectangle" }, { id: "text", label: "Text" }, { id: "arrow", label: "Arrow" }, ].map((t) => ( ))}
{/* Arrow Expression Editor */} {selectedShape && shapes.find((s) => s.id === selectedShape)?.type === "arrow" && (

Live Arrow Properties

{(() => { const arrow = shapes.find((s) => s.id === selectedShape) if (!arrow) return null const sourceShape = arrow.sourceShapeId ? shapes.find((s) => s.id === arrow.sourceShapeId) : null const targetShape = arrow.targetShapeId ? shapes.find((s) => s.id === arrow.targetShapeId) : null return (
From:{" "} {sourceShape?.text || sourceShape?.id || "None"}
To:{" "} {targetShape?.text || targetShape?.id || "None"}
{arrow.sourceShapeId && arrow.targetShapeId && ( <>
{ setShapes( shapes.map((s) => s.id === arrow.id ? { ...s, expression: e.target.value } : s, ), ) }} onBlur={() => { // Save to history when user finishes editing saveToHistory(shapes) }} className="w-full px-2 py-1 bg-slate-800 text-white rounded text-xs" placeholder="value: from.value * 2" />
)}
) })()}
)} {/* Shape Value Editor */} {selectedShape && shapes.find((s) => s.id === selectedShape)?.type !== "arrow" && (

Shape Properties

{(() => { const shape = shapes.find((s) => s.id === selectedShape) if (!shape) return null return (
{ setShapes( shapes.map((s) => s.id === shape.id ? { ...s, value: parseFloat(e.target.value) || 0 } : s, ), ) }} onBlur={() => { // Save to history when user finishes editing saveToHistory(shapes) }} className="w-full px-2 py-1 bg-slate-800 text-white rounded text-xs" />
Arrows connected from this shape will propagate this value.
) })()}
)} {/* Actions */}
) }