diff --git a/package.json b/package.json index c126abb8..76d54b6b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "caniuse-lite": "^1.0.30000697", "classnames": "^2.2.5", "d3-scale": "^3.3.0", + "file-saver": "^2.0.5", "firebase": "^7.14.0", "firebase-tools": "^7.14.0", "framer": "^1.1.7", @@ -43,7 +44,7 @@ "parcel-bundler": "^1.12.4", "parcel-plugin-html-externals": "^0.2.0", "postcss-preset-env": "^6.7.0", - "preshape": "^12.0.1", + "preshape": "^13.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "6.0.0-beta.2 ", @@ -51,12 +52,14 @@ "react-snap": "^1.23.0", "regl": "^2.0.1", "regression": "^2.0.1", - "sat": "^0.8.0" + "sat": "^0.8.0", + "uuid": "^8.3.2" }, "devDependencies": { "@types/bezier-easing": "*", "@types/classnames": "*", "@types/d3-scale": "*", + "@types/file-saver": "*", "@types/gl-matrix": "*", "@types/lodash.flatten": "*", "@types/lodash.floor": "*", @@ -69,6 +72,7 @@ "@types/react-router-dom": "*", "@types/regression": "^2.0.2", "@types/sat": "*", + "@types/uuid": "*", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "eslint": "^8.10.0", diff --git a/src/components/Landing/Landing.tsx b/src/components/Landing/Landing.tsx index 5af03ea8..59b64073 100644 --- a/src/components/Landing/Landing.tsx +++ b/src/components/Landing/Landing.tsx @@ -1,10 +1,15 @@ import { motion } from 'framer'; import { Box, Grid, Link, Text, Icons, useMatchMedia } from 'preshape'; import React from 'react'; -import data, { experienceSorted, listedWritingsSorted } from '../../data'; +import data, { + experienceSorted, + listedWritingsSorted, + publicationsSorted, +} from '../../data'; import Experience from '../Experience/Experience'; import Header from '../Header/Header'; import Project from '../Project/Project'; +import Publication from '../Publication/Publication'; import Writing from '../Writing/Writing'; export default function Landing() { @@ -67,9 +72,6 @@ export default function Landing() { Personal Projects - - Some of my favourite and finished personal side projects. - @@ -90,9 +92,6 @@ export default function Landing() { Experience - - A timeline of where and what I've worked on over the years. - {experienceSorted.map((exp, index) => ( @@ -106,11 +105,6 @@ export default function Landing() { Writings - - Usually when doing one of my side projects, I find something - to write about and then add them to this list. It's like an - infrequent blog with no consistent theme. - {listedWritingsSorted.map((writing) => ( @@ -123,12 +117,11 @@ export default function Landing() { Publications - - Usually when doing one of my side projects, I find something - to write about and then add them to this list. It's like an - infrequent blog with no consistent theme. - + + {publicationsSorted.map((publication) => ( + + ))} diff --git a/src/components/Projects/CircleArt/CircleArt.tsx b/src/components/Projects/CircleArt/CircleArt.tsx new file mode 100644 index 00000000..7ce16ebb --- /dev/null +++ b/src/components/Projects/CircleArt/CircleArt.tsx @@ -0,0 +1,34 @@ +import { Box, useMatchMedia } from 'preshape'; +import React, { useState } from 'react'; +import data from '../../../data'; +import ProjectPage from '../../ProjectPage/ProjectPage'; +import Editor from './Editor/Editor'; +import Gallery from './Gallery/Gallery'; +import configurations from './Gallery/configurations'; + +const CircleArt = () => { + const match = useMatchMedia(['1000px']); + const [circleData, setCircleData] = useState(configurations[0]); + + return ( + + + + + + + + + + + + ); +}; + +export default CircleArt; diff --git a/src/components/Projects/CircleArt/Editor/Editor.css b/src/components/Projects/CircleArt/Editor/Editor.css new file mode 100644 index 00000000..a7367ded --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/Editor.css @@ -0,0 +1,68 @@ +.CircleArt__circle, +.CircleArt__intersection { + stroke-width: 1; + transition-property: fill, stroke, stroke-width; + transition-duration: var(--transition-duration--fast); + transition-timing-function: var(--transition-timing-function); +} + +.CircleArt__circle--active { + stroke: var(--color-accent-shade-4); + stroke-width: 2; +} + +.CircleArt--mode-draw { + & .CircleArt__circle { + fill: transparent; + + &:hover { + stroke: var(--color-accent-shade-4); + } + } +} + + + + + + + + + + + +/* .CircleArt__circle--active, +.CircleArt__circle:hover { + stroke: var(--color-accent-shade-4); +} + +.CircleArt__circle--selectable, +.CircleArt__intersection--selectable { + stroke: var(--color-text-shade-4); +} + +.CircleArt__circle--selectable:hover, +.CircleArt__intersection--selectable:hover { + stroke: var(--color-accent-shade-4); +} */ + + + + + + + +.CircleArt--mode-fill, +.CircleArt--mode-view { + /* & .CircleArt__circle--selectable.CircleArt__circle--filled:hover, + & .CircleArt__intersection--selectable.CircleArt__intersection--filled:hover { + fill: var(--color-text-shade-1); + } */ +} + +/* .CircleArt--mode-view { + & .CircleArt__circle, + & .CircleArt__intersection { + stroke: rgba(var(--rgb-text-shade-3), 0.25); + } +} */ diff --git a/src/components/Projects/CircleArt/Editor/Editor.tsx b/src/components/Projects/CircleArt/Editor/Editor.tsx new file mode 100644 index 00000000..8600174d --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/Editor.tsx @@ -0,0 +1,641 @@ +import classNames from 'classnames'; +import FileSaver from 'file-saver'; +import { + Box, + themes, + TypeTheme, + useEventListener, + useResizeObserver, +} from 'preshape'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { v4 } from 'uuid'; +import useGraph, { Circle } from '../../IntersectionExplorer/useGraph'; +import { CircleArtData } from '../types'; +import { + CURSOR_DEFAULT, + CURSOR_DRAW, + CURSOR_FILL, + CURSOR_MOVE, + getCursor, + setCursor, +} from '../utils/cursor'; +import isPointOverCircleEdge from '../utils/math/isPointOverCircleEdge'; +import isPointWithinCircle from '../utils/math/isPointWithinCircle'; +import EditorCircle from './EditorCircle'; +import EditorControls from './EditorControls'; +import EditorIntersection from './EditorIntersection'; +import EditorToolbar from './EditorToolbar'; +import useEditorHistory from './useEditorHistory'; +import './Editor.css'; + +export type TypeMode = 'draw' | 'fill' | 'view'; + +type MinimalEvent = { + clientX: number; + clientY: number; + target: null | EventTarget; + type: string; +}; + +type Props = { + data: CircleArtData; +}; + +const COPY_OFFSET = 25; +const TOLERANCE_CREATE_CIRCLE = 30; +const TOLERANCE_SELECT_CIRCLE = 3; + +const onMouseDownGlobal = () => { + document.body.style.userSelect = 'none'; +}; + +const onMouseUpGlobal = () => { + document.body.style.userSelect = ''; +}; + +const minimalEvent = (event: TouchEvent | React.TouchEvent): MinimalEvent => ({ + clientX: event.touches[0]?.clientX, + clientY: event.touches[0]?.clientY, + target: event.target, + type: event.type, +}); + +export const getColors = (theme: TypeTheme, filled?: boolean) => { + return { + fill: filled + ? themes[theme].colorTextShade1 + : themes[theme].colorBackgroundShade1, + stroke: filled + ? themes[theme].colorBackgroundShade1 + : themes[theme].colorTextShade4, + }; +}; + +const Editor = ({ data }: Props) => { + const [{ width, height }, ref] = useResizeObserver(); + + const refActiveCircle = useRef(null); + const refCirclesContainer = useRef(null); + const refContainer = useRef(null); + const refIsAdding = useRef(false); + const refIsDirty = useRef(false); + const refIsMoving = useRef(false); + const refIsPointerDown = useRef(false); + const refIsResizing = useRef(false); + const refPointerPosition = useRef<[number, number]>([-1, -1]); + const refQueueActiveUpdate = useRef(false); + + const [activeCircle, setActiveCircle] = useState(null); + const [toolbarRect, setToolbarRect] = useState(null); + const [fills, setFills] = useState(data.fills); + const [mode, setMode] = useState('view'); + + const [circles, setCircles] = useState([]); + const editorHistory = useEditorHistory(); + + const { graph } = useGraph(circles, { + findTraversalsOnUpdate: mode === 'fill' || mode === 'view', + }); + + const getScaledCircles = (circles: Circle[]) => { + const scale = Math.min(width / data.width, height / data.height); + const tx = (width - data.width * scale) * 0.5; + const ty = (height - data.height * scale) * 0.5; + + return circles.map(({ id, radius, x, y }) => ({ + id, + radius: radius * scale, + x: x * scale + tx, + y: y * scale + ty, + })); + }; + + const handleSaveJSON = () => { + FileSaver.saveAs( + new Blob( + [ + JSON.stringify( + { + height, + width, + circles, + fills, + }, + null, + 2 + ), + ], + { + type: 'text/json;charset=utf-8', + } + ), + `CircleArt-export_${Date.now()}.json` + ); + }; + + const handleSavePNG = () => { + if (refContainer.current) { + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(refContainer.current); + + FileSaver.saveAs( + new Blob( + [ + ` +${svgString} +`, + ], + { + type: 'text/svg;charset=utf-8', + } + ), + `CircleArt-export_${Date.now()}.svg` + ); + } + }; + + const handleClear = () => { + setCircles([]); + setMode('draw'); + handleSetActiveCircle(null); + handleSetToolbarRect(null); + editorHistory.commit(); + }; + + const handleSetMode = (nextMode: TypeMode) => { + const currentMode = mode; + refIsDirty.current = true; + editorHistory.push( + () => { + setMode(nextMode); + handleSetActiveCircle(null); + handleSetToolbarRect(null); + }, + { + undo: () => { + setMode(currentMode); + }, + } + ); + }; + + const handleSetToolbarRect = (circle: Circle | null) => { + if (circle && refContainer.current) { + const { left, top } = refContainer.current.getBoundingClientRect(); + const { x, y, radius } = circle; + + setToolbarRect( + new DOMRect( + left + (x - radius), + top + (y - radius), + radius * 2, + radius * 2 + ) + ); + } else { + setToolbarRect(null); + } + }; + + const getNewCircle = ( + x: number, + y: number, + radius: number = 0, + id = v4() + ): Circle => ({ + id, + radius, + x, + y, + }); + + const getActiveCircleElement = () => { + if (refCirclesContainer.current && refActiveCircle.current) { + for (const element of refCirclesContainer.current.children) { + if (element.id === refActiveCircle.current.id) { + return element; + } + } + } + + return null; + }; + + const getCircleAtCoordinates = useCallback( + (x: number, y: number, padding: number) => { + for (let i = graph.circles.length - 1; i >= 0; i--) { + const { x: cx, y: cy, radius } = graph.circles[i]; + + if (isPointWithinCircle(x, y, cx, cy, radius, padding)) { + return graph.circles[i]; + } + } + + return null; + }, + [graph.circles] + ); + + const getRelativeCoordinates = ({ clientX, clientY }: MinimalEvent) => { + if (refContainer.current) { + const { left, top } = refContainer.current.getBoundingClientRect(); + const x = clientX - left; + const y = clientY - top; + const [startX, startY] = refPointerPosition.current; + const deltaX = x - startX; + const deltaY = y - startY; + + return { + deltaX, + deltaY, + x, + y, + }; + } + + return { + deltaX: 0, + deltaY: 0, + x: 0, + y: 0, + }; + }; + + const handleSetActiveCircle = (circle: Circle | null) => { + refActiveCircle.current = circle; + setActiveCircle(circle); + }; + + const handleAddCircle = (circle: Circle) => { + setCircles((circles) => [...circles, circle]); + handleSetActiveCircle(circle); + }; + + const handleRemoveCircle = (circle: Circle) => { + editorHistory.push( + () => { + setCircles((circles) => circles.filter(({ id }) => circle.id !== id)); + handleSetActiveCircle(null); + handleSetToolbarRect(null); + }, + { + undo: () => { + handleAddCircle(circle); + }, + } + ); + }; + + const handleUpdateCircle = (update: Circle) => { + setCircles((circles) => + circles.map((circle) => (circle.id === update.id ? update : circle)) + ); + }; + + const handleCopyActiveCircle = () => { + if (refActiveCircle.current) { + const circle = getNewCircle( + refActiveCircle.current.x + COPY_OFFSET, + refActiveCircle.current.y + COPY_OFFSET, + refActiveCircle.current.radius + ); + + editorHistory.push( + () => { + handleAddCircle(circle); + handleSetToolbarRect(circle); + }, + { + undo: () => handleRemoveCircle(circle), + } + ); + } + }; + + const handleMoveActiveCircle = (deltaX: number, deltaY: number) => { + const circle = refActiveCircle.current; + const element = getActiveCircleElement(); + + if (circle && element) { + const x = circle.x + deltaX; + const y = circle.y + deltaY; + + element.setAttribute('cx', x.toString()); + element.setAttribute('cy', y.toString()); + + refQueueActiveUpdate.current = true; + } + }; + + const handleRemoveActiveCircle = () => { + if (refActiveCircle.current) { + handleRemoveCircle(refActiveCircle.current); + } + }; + + const handleResizeActiveCircle = (x: number, y: number) => { + const circle = refActiveCircle.current; + const element = getActiveCircleElement(); + + if (circle && element) { + const radius = Math.hypot(x - circle.x, y - circle.y); + + element.setAttribute('r', radius.toString()); + + refQueueActiveUpdate.current = true; + } + }; + + const handleToggleFilled = (id: string) => { + editorHistory.push( + () => { + setFills((fills) => ({ ...fills, [id]: !fills[id] })); + }, + { + undo: () => { + setFills((fills) => ({ ...fills, [id]: !fills[id] })); + }, + } + ); + }; + + const handleMouseDown = useCallback( + (event: MinimalEvent) => { + refIsPointerDown.current = true; + onMouseDownGlobal(); + + if (mode === 'draw') { + const { x, y } = getRelativeCoordinates(event); + const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE); + + handleSetActiveCircle(circle); + + if (circle) { + refPointerPosition.current = [x, y]; + + if ( + isPointOverCircleEdge( + x, + y, + circle.x, + circle.y, + circle.radius, + TOLERANCE_SELECT_CIRCLE + ) + ) { + refIsResizing.current = true; + } else { + refIsMoving.current = true; + } + } + } + }, + [getCircleAtCoordinates, mode] + ); + + const handleMouseUp = useCallback(() => { + onMouseUpGlobal(); + + if (refActiveCircle.current) { + if (refQueueActiveUpdate.current) { + const element = getActiveCircleElement(); + + if (element) { + const currentCircle = refActiveCircle.current; + const updatedCircle: Circle = { + id: currentCircle.id, + radius: parseFloat(element.getAttribute('r') || '0'), + x: parseFloat(element.getAttribute('cx') || '0'), + y: parseFloat(element.getAttribute('cy') || '0'), + }; + + const update = (circle: Circle) => { + handleUpdateCircle(circle); + handleSetActiveCircle(circle); + handleSetToolbarRect(circle); + }; + + if (updatedCircle.radius < TOLERANCE_CREATE_CIRCLE) { + handleRemoveActiveCircle(); + } else if (refIsAdding.current) { + editorHistory.push(() => update(updatedCircle), { + undo: () => handleRemoveCircle(updatedCircle), + redo: () => { + handleAddCircle(updatedCircle); + handleSetToolbarRect(updatedCircle); + }, + }); + } else { + editorHistory.push(() => update(updatedCircle), { + undo: () => update(currentCircle), + }); + } + } + } else { + handleSetToolbarRect(refActiveCircle.current); + } + } else { + handleSetToolbarRect(null); + } + + refIsAdding.current = false; + refIsPointerDown.current = false; + refIsMoving.current = false; + refIsResizing.current = false; + }, [handleUpdateCircle, mode]); + + const handleTouchEnd = useCallback( + (event: TouchEvent) => { + handleMouseUp(); + + if (refContainer.current?.contains(event.target as Node)) { + event.preventDefault(); + } + }, + [handleMouseUp] + ); + + const handleDrag = useCallback( + (event: MinimalEvent) => { + if (mode === 'draw') { + const { deltaX, deltaY, x, y } = getRelativeCoordinates(event); + + if (refActiveCircle.current) { + setToolbarRect(null); + + if (refIsResizing.current) { + handleResizeActiveCircle(x, y); + } else { + handleMoveActiveCircle(deltaX, deltaY); + } + } else if (Math.hypot(deltaX, deltaY) > TOLERANCE_CREATE_CIRCLE) { + handleAddCircle(getNewCircle(x, y)); + refIsAdding.current = true; + refIsResizing.current = true; + } + } + }, + [mode] + ); + + const handleMouseMoveDraw = useCallback( + (event: MinimalEvent) => { + const { x, y } = getRelativeCoordinates(event); + const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE); + + if (circle) { + if ( + isPointOverCircleEdge( + x, + y, + circle.x, + circle.y, + circle.radius, + TOLERANCE_SELECT_CIRCLE + ) + ) { + setCursor(refContainer.current, getCursor(x, y, circle.x, circle.y)); + } else { + setCursor(refContainer.current, CURSOR_MOVE); + } + } else { + setCursor(refContainer.current, CURSOR_DRAW); + } + }, + [getCircleAtCoordinates] + ); + + const handleMouseMoveFill = useCallback( + (event: MinimalEvent) => { + const { x, y } = getRelativeCoordinates(event); + const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE); + + if (circle) { + setCursor(refContainer.current, CURSOR_FILL); + } else { + setCursor(refContainer.current, CURSOR_DEFAULT); + } + }, + [getCircleAtCoordinates] + ); + + const handleMouseMove = useCallback( + (event: MinimalEvent) => { + if (refIsPointerDown.current) { + return handleDrag(event); + } + + if (mode === 'draw') handleMouseMoveDraw(event); + if (mode === 'fill') handleMouseMoveFill(event); + }, + [handleDrag, handleMouseMoveDraw, handleMouseMoveFill, mode] + ); + + useEffect(() => { + if (width && height) { + setCircles(getScaledCircles(data.circles)); + setFills(data.fills); + } + }, [data, width, height]); + + useEventListener(document, 'mouseup', handleMouseUp); + useEventListener(document, 'mousemove', handleMouseMove); + useEventListener(document, 'touchend', handleTouchEnd); + + const classes = classNames('CircleArt', `CircleArt--mode-${mode}`); + + return ( + + + handleMouseMove(minimalEvent(e))} + onTouchStart={(e) => handleMouseDown(minimalEvent(e))} + preserveAspectRatio="xMidYMid meet" + ref={refContainer} + tag="svg" + viewBox={`0 0 ${width} ${height}`} + width={width} + > + {mode === 'draw' && ( + + {graph.circles + .sort((a, b) => b.radius - a.radius) + .map(({ id, radius, x, y }) => ( + + ))} + + )} + + {(mode === 'fill' || mode === 'view') && ( + + {graph.traversals.map((traversal) => ( + handleToggleFilled(traversal.bitset.toString()) + : undefined + } + traversal={traversal} + /> + ))} + + {graph.circles + .filter((_, i) => + graph.edges.every(({ circle }) => circle !== i) + ) + .map(({ id, radius, x, y }) => ( + handleToggleFilled(id) + : undefined + } + radius={radius} + x={x} + y={y} + /> + ))} + + )} + + + + + + + + ); +}; + +export default Editor; diff --git a/src/components/Projects/CircleArt/Editor/EditorCircle.tsx b/src/components/Projects/CircleArt/Editor/EditorCircle.tsx new file mode 100644 index 00000000..ea0e848b --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/EditorCircle.tsx @@ -0,0 +1,35 @@ +import classNames from 'classnames'; +import { Box } from 'preshape'; +import React, { useContext } from 'react'; +import { RootContext } from '../../../Root'; +import { Circle } from '../../IntersectionExplorer/useGraph'; +import { getColors } from './Editor'; + +type Props = Circle & { + active?: boolean; + filled?: boolean; + onClick?: () => void; +}; + +const EditorCircle = ({ active, filled, id, onClick, radius, x, y }: Props) => { + const { theme } = useContext(RootContext); + const classes = classNames('CircleArt__circle', { + 'CircleArt__circle--active': active, + 'CircleArt__circle--selectable': onClick, + }); + + return ( + + ); +}; + +export default EditorCircle; diff --git a/src/components/Projects/CircleArt/Editor/EditorControls.tsx b/src/components/Projects/CircleArt/Editor/EditorControls.tsx new file mode 100644 index 00000000..971e9b02 --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/EditorControls.tsx @@ -0,0 +1,154 @@ +import { useMatchMedia, Button, Buttons, Box, Icons } from 'preshape'; +import React from 'react'; +import { TypeMode } from '../types'; + +const canSave = typeof window !== 'undefined' && window.Blob !== undefined; + +interface Props { + canRedo: boolean; + canUndo: boolean; + mode: TypeMode; + onChangeMode: (mode: TypeMode) => void; + onClear: () => void; + onRedo: () => void; + onSaveJSON: () => void; + onSavePNG: () => void; + onUndo: () => void; +} + +const EditorControls = (props: Props) => { + const match = useMatchMedia(['600px', '800px']); + const { + canUndo, + canRedo, + mode, + onChangeMode, + onClear, + onRedo, + onSaveJSON, + onSavePNG, + onUndo, + } = props; + + return ( + event.nativeEvent.stopPropagation()} + padding="x6" + width="100%" + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EditorControls; diff --git a/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx b/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx new file mode 100644 index 00000000..78d1e0a7 --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import { Box } from 'preshape'; +import React, { useContext } from 'react'; +import { RootContext } from '../../../Root'; +import getTraversalPath from '../../IntersectionExplorer/GraphVisualisation/getTraversalPath'; +import { Graph, Traversal } from '../../IntersectionExplorer/useGraph'; +import { getColors } from './Editor'; + +type Props = { + filled?: boolean; + graph: Graph; + onClick?: () => void; + traversal: Traversal; +}; + +const EditorIntersection = ({ filled, graph, onClick, traversal }: Props) => { + const { theme } = useContext(RootContext); + const classes = classNames('CircleArt__intersection', { + 'CircleArt__intersection--selectable': onClick, + }); + + const d = getTraversalPath(traversal, graph.nodes, graph.edges); + + return ( + + ); +}; + +export default EditorIntersection; diff --git a/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx b/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx new file mode 100644 index 00000000..5c017c74 --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx @@ -0,0 +1,73 @@ +import { ReferenceObject } from 'popper.js'; +import { + Button, + Buttons, + Icons, + Placement, + PlacementArrow, + PlacementContent, +} from 'preshape'; +import React, { useEffect, useState } from 'react'; + +interface Props { + rect: DOMRect | null; + onCopy: () => void; + onDelete: () => void; +} + +class Reference implements ReferenceObject { + rect: DOMRect; + + constructor(rect: DOMRect) { + this.rect = rect; + } + + get clientHeight() { + return this.rect.height; + } + + get clientWidth() { + return this.rect.width; + } + + getBoundingClientRect(): DOMRect { + return this.rect; + } +} + +export default function EditorToolbar({ rect, onCopy, onDelete }: Props) { + const [referenceElement, setReferenceElement] = useState(); + + useEffect(() => { + if (rect) { + setReferenceElement(new Reference(rect)); + } + }, [rect]); + + return ( + + + + + + + + + + + ); +} diff --git a/src/components/Projects/CircleArt/Editor/useEditorHistory.ts b/src/components/Projects/CircleArt/Editor/useEditorHistory.ts new file mode 100644 index 00000000..ff2a170c --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/useEditorHistory.ts @@ -0,0 +1,86 @@ +import { useRef, useState } from "react"; + +interface Action { + undo: () => void; + redo?: () => void; +} + +interface Store { + history: Action[]; + future: Action[]; +} + +const useEditorHistory = () => { + const refAllowPush = useRef(true); + const [store, setStore] = useState({ + history: [], + future: [], + }); + + const commit = () => { + setStore({ + history: [], + future: [], + }) + }; + + const push = (action: () => void, { undo, redo = action }: Action) => { + action(); + + if (refAllowPush.current) { + setStore((store) => ({ + history: [...store?.history, { undo, redo }], + future: [], + })); + } + }; + + const pop = () => { + setStore((store) => { + const action = store.history[store.history.length - 1]; + + if (action) { + refAllowPush.current = false; + action.undo(); + refAllowPush.current = true; + + return { + history: store.history.slice(0, -1), + future: [...store.future, action], + }; + } + + return store; + }); + }; + + const replay = () => { + setStore((store) => { + const action = store.future[store.future.length - 1]; + + if (action) { + refAllowPush.current = false; + action.redo?.(); + refAllowPush.current = true; + + return { + history: [...store.history, action], + future: store.future.slice(0, -1), + }; + } + + return store; + }); + }; + + return { + canUndo: !!store.history.length, + canRedo: !!store.future.length, + commit, + push, + pop, + replay, + }; +}; + +export default useEditorHistory; diff --git a/src/components/Projects/CircleArt/Gallery/Gallery.tsx b/src/components/Projects/CircleArt/Gallery/Gallery.tsx new file mode 100644 index 00000000..b6b4e015 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/Gallery.tsx @@ -0,0 +1,37 @@ +import { Button, Grid, Image } from 'preshape'; +import React from 'react'; +import { CircleArtGalleryItem } from '../types'; +import configurations from './configurations'; + +const Gallery = ({ + onSelect, +}: { + onSelect: (data: CircleArtGalleryItem) => void; +}) => { + return ( + + {configurations.map((data) => ( + + ))} + + ); +}; + +export default Gallery; diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Fox.json b/src/components/Projects/CircleArt/Gallery/configurations/Fox.json new file mode 100644 index 00000000..7fdd718c --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Fox.json @@ -0,0 +1,117 @@ +{ + "height": 774, + "width": 1584, + "circles": [ + { + "id": "ee54790d-d2e9-4c9e-972d-e3925352f79f", + "radius": 358.0348463487878, + "x": 1111.92, + "y": 399.9000000000001 + }, + { + "id": "a61260ca-57f2-4ade-b436-ce039f746fa9", + "radius": 191.35531453293902, + "x": 793.2899999999998, + "y": 316.05 + }, + { + "id": "3200307b-1b83-49dc-b534-b725d22d1d61", + "radius": 188.33116417629876, + "x": 790.71, + "y": 332.82000000000005 + }, + { + "id": "cf1e907e-e3fb-43fc-9dc9-13908b7e0ada", + "radius": 183.5656855188355, + "x": 790.71, + "y": 468.27000000000004 + }, + { + "id": "ccc62704-3292-4d97-a2b1-c16263e76279", + "radius": 182.17796354114842, + "x": 802.32, + "y": 319.92 + }, + { + "id": "69032566-2f78-4c25-8bf4-9d0846cc9aa7", + "radius": 179.3285601905062, + "x": 806.19, + "y": 145.77 + }, + { + "id": "633dc374-1b10-4e60-a947-6a9cb3bb5035", + "radius": 173.78172170858477, + "x": 794.58, + "y": 461.82000000000005 + }, + { + "id": "e7e1d120-f468-4b86-a0a9-8b945ad0d35f", + "radius": 149.3060065770966, + "x": 865.5300000000001, + "y": 438.6 + }, + { + "id": "e223d3c4-34ee-430f-99f8-1ae752fe1256", + "radius": 149.0215236132016, + "x": 839.73, + "y": 712.08 + }, + { + "id": "c700ab07-2d3c-4b32-b1d2-0ba1a3a69cc6", + "radius": 79.06973947092531, + "x": 942.9299999999998, + "y": 175.44000000000003 + }, + { + "id": "dcb54c82-fdd6-412b-b405-abe39b5c4d8c", + "radius": 30.44509320071134, + "x": 870.6900000000003, + "y": 224.46 + }, + { + "id": "f607fad9-31d0-4a2f-ac36-c3933290fcec", + "radius": 30.44509320071134, + "x": 864.24, + "y": 273.48 + } + ], + "fills": { + "1000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000100010000000000000000000000000000010100000000000000000000000000000000000000": false, + "100000000000000000000000000000001000000000000000000000000000000000000000000000001000000000001000000000000000000000000000000000000000000000000000010000000000000000000100000000000100000100000000000000000000000000000000": true, + "1000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000000010100000000010100000000000000000000000000": true, + "10000000000000000000000000000000000000000000000010000000000001000000000000001000000000000000000000000000000000000000000000000000000000000000000000100000100000100000100000000000000000000": true, + "100000000000000000000000000000000100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000010000000000000000000100000000000000000000000000000000000000100000100000": true, + "1000000000000000010000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000100010000000000000000000000000000000000000000000000000000000010100000000000": true, + "10000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000100000000000100000000000000000000000000000000010000000000000000000000000100000000001000": true, + "1000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000010000000000000000001000000000000000000000000000000000000000000000010000000000010000000000000000000000001010": true, + "1000000000000000000001000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000001000100000000000000000000000000000000000000000000000000000000010100000000000": true, + "10000000000000000000000000000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000100000000000100000000000000000001000000000000000000000000000000000000000100000100000": true, + "10000000000000000000000010000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100100000000000000001001000000000000000000000000000000000000000000000000": true, + "1000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000100100000000000000000000000000000000000000000000000000000000000000000000": true, + "100000000000000000000101000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000100000000001000000100000000000000000000000000001000000000000000000000000100000000001000": true, + "11000000000000000000000000000000000000000000000001000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000001000000001000001000000000000000000000000000000000": true, + "100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000001000000000000000000000000000000000000000000000000000000000000110000000000000": true, + "100000000000000000000000000000000000000000100000000000000000000000000000001000000000000001000000000000000001000000000000000000000000000001000000000000000001000000000000000010000000000000000001010": true, + "1000000000000000000000000000000000000000000000001000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000010000010": true, + "1000000000001000000000000000000000100000000000000001000000000000000000000000000000000000000000000000100000000000000100000000000000000000000011000000000000000000000000000000000001000010000000001000010000000": true, + "1000000000000000000000010000000000000000000000001000000000000000000000000000000000000000010000000000000000000000000000000000000001000000000000000010000000000000000001000001000000000000000000000": true, + "1000000000000000000000000000000000010000000000000101000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011001000011000000000000000000000000000000000000000000000000000000000000000": true, + "1000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000010000000000000000000000000000000000000000000000000001001000000000": true, + "10000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000001000010000000000000000000000000000000000000000000000000000011000000000": true, + "100000000000000000000000000000000000100000000000000000100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000100000000000000100000001000000000000000100000000000000000000000000000000000000000010000010000": true, + "1000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000010000000000100000000000010000010000000000000000000000000000000000000000000000": true, + "100000000001000000001000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000001010010000000001100000000000000000000000000000000000000000000000000000000000000000": true, + "101000000010000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001100010000000000000000000000100010000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000010010000000000000000000000000000000000000000000000000": true, + "10100000000000000000000000000000001000000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000100011000000000000000000010010000000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000001000000001000000000010000010000000000000000000000000000000000000000000000": true, + "1000000100000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000100000000000000000000000000000000001000000000000101000000000000000000000000100000000010000000000000000000000000000000000000010100": true, + "1000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000010000000000000000000000000000000010000000000000000010000000000000000000000000000000000001000100": true, + "10000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000010000000000000000100000000000000000000000000000000010000000000000000000000000000010000000000000000000000001000001": true, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000000000000000000000000000000000000000010000000000000100000000000000001": true, + "10000000101000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100010001000000000000000000000000000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000000000010000100000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000100001000001000000000000000000000000000000000": true + } +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg b/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg new file mode 100644 index 00000000..62ea7f07 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Island.json b/src/components/Projects/CircleArt/Gallery/configurations/Island.json new file mode 100644 index 00000000..eff61910 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Island.json @@ -0,0 +1,121 @@ +{ + "height": 784, + "width": 1004, + "circles": [ + { + "id": "2852ff49-f1fb-4167-8e98-2023db8f93cd", + "radius": 501.8756811092652, + "x": 894.6533333333334, + "y": 104.5333333333333 + }, + { + "id": "591de2b9-3e33-43c5-8bc8-b3c1a2eabe5c", + "radius": 501.8756811092652, + "x": 139.40000000000003, + "y": 240.42666666666665 + }, + { + "id": "4543bb1f-c8a6-4d68-9cba-5a67dc4e6a67", + "radius": 501.8756811092652, + "x": 127.63999999999999, + "y": 186.85333333333338 + }, + { + "id": "0d4c6dfa-4a13-41ce-8225-d221d80d245c", + "radius": 501.8756811092652, + "x": 890.7333333333333, + "y": 185.54666666666668 + }, + { + "id": "990b49c0-6eff-4e07-9f34-b7bc2ac06c15", + "radius": 224.99722467424152, + "x": 505.26666666666665, + "y": 659.8666666666667 + }, + { + "id": "e11238af-ba30-4671-930f-2500116bdf7f", + "radius": 180.6416454998373, + "x": 502.65333333333325, + "y": 593.2266666666667 + }, + { + "id": "4d3567a4-92a2-473e-94c0-000067ca7363", + "radius": 143.82833224214053, + "x": 442.5466666666667, + "y": 354.1066666666667 + }, + { + "id": "925a9720-2ede-424e-83ba-cff156993f47", + "radius": 143.82833224214053, + "x": 540.5466666666667, + "y": 355.4133333333333 + }, + { + "id": "9f1f70fa-c2c2-4f82-8356-7c56291f8bb5", + "radius": 143.82833224214053, + "x": 660.76, + "y": 395.92 + }, + { + "id": "2e10d006-252c-4877-8927-144df6b1d640", + "radius": 143.82833224214053, + "x": 368.0666666666667, + "y": 350.18666666666667 + }, + { + "id": "27deddad-8512-449c-93dc-8953406ae242", + "radius": 143.82833224214053, + "x": 510.49333333333345, + "y": 287.46666666666664 + }, + { + "id": "492793ee-8f36-451b-aef8-d909dcdc2e1c", + "radius": 123.27068679220626, + "x": 601.96, + "y": 373.7066666666667 + }, + { + "id": "3d61e795-512c-46ab-95b8-8f4c33efa1a4", + "radius": 115.94043834850913, + "x": 611.1066666666668, + "y": 348.88 + }, + { + "id": "8b400b49-2250-4f8e-be34-d7b9c96ff4a2", + "radius": 113.32650018616319, + "x": 402.04, + "y": 313.6 + }, + { + "id": "979174bc-f4e7-47e1-98fe-b7183f51af2e", + "radius": 113.32650018616319, + "x": 438.62666666666667, + "y": 292.6933333333333 + }, + { + "id": "35cc5f6f-50ed-460f-977b-612f22e1b08a", + "radius": 80.09131233230676, + "x": 349.7733333333333, + "y": 266.56 + }, + { + "id": "533bb888-842d-4c04-91a2-e2e00cc3680d", + "radius": 63.71926204497001, + "x": 450.3866666666667, + "y": 209.06666666666666 + }, + { + "id": "df4cf2af-6650-48e3-a831-0b298be07269", + "radius": 63.71926204497001, + "x": 579.7466666666667, + "y": 270.47999999999996 + }, + { + "id": "2667bac5-438b-4fc6-a800-fd14a78d869e", + "radius": 63.71926204497001, + "x": 672.52, + "y": 297.92 + } + ], + "fills": {} +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Island.svg b/src/components/Projects/CircleArt/Gallery/configurations/Island.svg new file mode 100644 index 00000000..474a86f5 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Island.svg @@ -0,0 +1 @@ + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json new file mode 100644 index 00000000..baf82f46 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json @@ -0,0 +1,113 @@ +{ + "height": 774, + "width": 1584, + "circles": [ + { + "id": "35c1f1a3-e368-4710-9a20-44d34f95de81", + "radius": 396.45836767559854, + "x": 434.66999999999996, + "y": 430.85999999999996 + }, + { + "id": "0f2557b2-765b-4046-951d-5f98cb021887", + "radius": 187.49429991335737, + "x": 795.87, + "y": 433.44000000000005 + }, + { + "id": "9b8d8361-e3de-49e6-bb86-9bd0ab8b2998", + "radius": 160.58298041822488, + "x": 782.9699999999999, + "y": 419.24999999999994 + }, + { + "id": "1ed1e95c-8dda-4cae-979e-8bb76b80bd4f", + "radius": 160.0432041668749, + "x": 799.74, + "y": 399.90000000000003 + }, + { + "id": "eda3812c-0914-4877-a97c-500e45fe0288", + "radius": 83.22249816005284, + "x": 797.16, + "y": 250.26 + }, + { + "id": "8a2b227a-c023-415a-a8b0-6e568d975cc5", + "radius": 70.1005513530386, + "x": 797.16, + "y": 250.26 + }, + { + "id": "0dc8128d-a9e9-42d9-b08c-b379f9a5661c", + "radius": 36.14302837339451, + "x": 762.3299999999999, + "y": 248.97 + }, + { + "id": "c6479e94-eb57-470a-bf31-16d51816d793", + "radius": 36.14302837339451, + "x": 830.7, + "y": 248.97 + }, + { + "id": "5edefc6f-5199-40b3-bbe3-2bf1de104d36", + "radius": 27.21257981155039, + "x": 882.3, + "y": 247.68 + }, + { + "id": "ca14669b-5e67-4e2c-b6c0-041296553c8d", + "radius": 27.21257981155039, + "x": 710.7299999999999, + "y": 247.68 + }, + { + "id": "b85b57f8-b3bd-4f40-b521-cc26868e690a", + "radius": 8.260030266288375, + "x": 768.78, + "y": 254.12999999999997 + }, + { + "id": "28e1cd33-06ea-4ec9-9268-34cbe0099154", + "radius": 8.260030266288375, + "x": 825.54, + "y": 254.12999999999997 + } + ], + "fills": { + "100000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000000000000000010000000000000000000000000010000000000001000000000000000000010000000000010000000000000": true, + "1000000000000000000000000000000000000000000000100000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000010000010000000000000000000000000000000000000000000000001010000000": true, + "1000000001000000000001000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000100010001000000000000000000000000000000000000000000000000000101000000000": true, + "10000000000000000000000000000000000100000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000100010000000000000000000000000000000000000000000000000000101000000000": true, + "10000000000000000000000000000000001000000000000010000000000000000000000000000000000000000000000000000000000000000000000001000000000001000001000000000000000000000000000000000000000000000001010000000": true, + "1000000010000000000000000000000010000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000001000000000000100010000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000000000000000100000000000001000000000000000000000000000000000000000000000000000000000000000000000000000001000010000000100000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000010000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000000000001000001000000000010100000000000000000000000000000000000000000000": true, + "1000000000001000100000000000000000000000000000000010000000000000000000000000000000000000000000000000000010100000000000000000000000000010100000000000000": true, + "10000000000000000000000000000000000000010000000000010000000000000000000000000000000000000010000000000000000000000000000000000000100000100000000000000000000000000000000000000010100000000000000": true, + "10000000000000000000000000000000000000100000000000100000000000000000000010000000000000000000000000000000000000000000000000000000100000100000000000000000000010100000000000000000000000000000000": true, + "1000000000000000000000000000000000000000000000000010000000000000000000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000010000000100000000000000": true, + "100000000000000000000000000000000000000000000000001001000000000000000000000000000000000100000000000000000000000000000000000000000000000010000000100000000000000000000010000000100000000000000": true, + "1000000000001000000000000000000000010000000000000000000000000000000000010000000000000000000000000000000000000000000000010100000000000000000000000101000000": true, + "10000000000010000000000000000000000000000000000000000000100000000000000000000000000001000000000000000000000000000000000000000000010100000000000000000000000010100000000": false, + "10000000000000000000000000000000000000000000000010000000000010000000000000000000000000000000010000000000000000000000000010000000000000000000000100000000000000000010000100000000000000000000000000101000000": true, + "10000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000001000000000000000000000000000000000000000000000000100000000000000000010000001000000000000000000000000000000000": true, + "1000000000000000000000000000000010000000000001000000000001000000000000000000000000000000000001000000000000000000001000000000000000000000100010000000000000000000010000001000000000000010001000000000000000": true, + "1000000000000000000000000000000000001000000000010000000000000100000000000000000000100000000000000000000000000000000000000000000000000000000000010001010000000010001000000000000000000000000000000000000000000000": true, + "1000000000000000000000000000000000000000000000000000000000001000000000000000000001000000000000000100000000000000000000100000000000000000000000000000010000000000000100000001000000000000000001010000000000000": true, + "10000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000010000000000000000000000000000010000000000000000000000000000010000000000010000000000000": true, + "1010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000010000000000000000000000000000000000000000000000011000000000000001100000000000000000000000000": true, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000": true, + "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000": true, + "100000000000001000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000101000000000001000000000000000000000001000000000000000000000000001101000000000000000000100010": false, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000001000000000000000000001000000000010000000000000000010000000000000000000000010000010000000000000000000000101": true, + "100000000000000000000000000000000110000000000000000000000000000010000000000000000000000000000000000000000001000000000000000001000000000101": true, + "1000000000000000000000000000000000000000000000000100100000000000000000100000000000000001000000000000000000000000000000000000000000000100000000000000000000000101000000010000000001000000000000": true, + "1000000000000000000001100000000000000000000000000000000000100000000000000000000000000000000000000000000000000101000000000000000000000001010000": true, + "1000000000000000100000000000000000000000000000001000000000000000000000000000000000000000001000000000000000000000000010100": false, + "100000000000000000000000010000000000000000000100000000000000000000000000000000000001000000000000000000000000000000000000010001000000000000000000000000010001000000000000000": true + } +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg new file mode 100644 index 00000000..10882ea5 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json new file mode 100644 index 00000000..8d3031c9 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json @@ -0,0 +1,144 @@ +{ + "height": 774, + "width": 1584, + "circles": [ + { + "id": "1df7d3a3-4c0d-45f3-8b9b-87d13bbf3f8b", + "radius": 360.84276977653303, + "x": 796.515, + "y": 7.740000000000031 + }, + { + "id": "08ff5e5c-cb6d-44ec-90f6-340c0ffffc73", + "radius": 337.1740199066352, + "x": 886.8149999999999, + "y": 492.78000000000003 + }, + { + "id": "1d747159-7574-4ab1-b953-a0cbfac1a642", + "radius": 337.1740199066352, + "x": 698.475, + "y": 487.62 + }, + { + "id": "ef949e75-d9a4-4392-b0c7-75697ade97bb", + "radius": 267.57784381372085, + "x": 796.515, + "y": 546.96 + }, + { + "id": "ee574b9a-1a09-4fb4-b900-81f38fde503d", + "radius": 267.57784381372085, + "x": 797.8049999999998, + "y": 86.43000000000002 + }, + { + "id": "928bdb76-8588-42b5-b0e7-5e705bc59c97", + "radius": 240.5945105358807, + "x": 1076.4449999999997, + "y": 381.84000000000003 + }, + { + "id": "5388d1bd-df59-4cbb-bb00-f21fefde44f4", + "radius": 240.5945105358807, + "x": 516.585, + "y": 380.54999999999995 + }, + { + "id": "b686b11a-2510-447c-9868-7d33a5e4820c", + "radius": 136.33780473515043, + "x": 796.515, + "y": 310.89 + }, + { + "id": "7030d33d-1afe-4275-ada4-39c73abb052f", + "radius": 114.92589655947873, + "x": 793.935, + "y": 443.76 + }, + { + "id": "5cd70382-7abe-44c4-9f01-155c355f974c", + "radius": 50.72177638845075, + "x": 796.515, + "y": 344.43 + }, + { + "id": "335db4a2-2676-47ae-ab0b-28507f5d5eee", + "radius": 45.150000000000006, + "x": 922.9349999999998, + "y": 297.99 + }, + { + "id": "c7e3d719-5b00-453d-9766-2ff4c399d9d1", + "radius": 44.12478895133664, + "x": 667.5149999999999, + "y": 296.70000000000005 + }, + { + "id": "ead0d0d9-0284-4588-afe3-6bf8ea775419", + "radius": 38.20229966899898, + "x": 760.395, + "y": 513.42 + }, + { + "id": "cc02c054-6f3d-4142-a975-e6c513b09961", + "radius": 37.41, + "x": 831.345, + "y": 512.13 + }, + { + "id": "b6203c37-baf0-475b-b585-44945f73f604", + "radius": 16.317352726468837, + "x": 897.135, + "y": 316.05 + }, + { + "id": "d04a8040-6c52-4665-bd95-0f860d302a88", + "radius": 15.746456744296477, + "x": 694.605, + "y": 316.05 + } + ], + "fills": { + "10101000000000000000000010000000000000000100000000000000000000000000000000000000000100000000000000000001000000000000010000000000000000000000000000000000000000000000000000100000000100000000000000000000000001100000110000000000110000000000000000000000000000": true, + "100000000000000000000000000000000000000000000000000000100000000000000000010000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000010000000000100000000000000000000000000001000001000000000000000000000000000000000000000000000000": true, + "1000000000000000000100000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000100000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000010000110001000000001000000000000000000000000000000000000000000000": true, + "10100000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000010000000000000000000001000000010000000000000000000000000000000011000000000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000000100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000010000000000100000000000000000000000000000000000000000000000000001000100000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000001000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010000000010001000000000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000010000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000001000001000000000000000000000000000000000000000000000": true, + "10000100000000000000000000000000000000000000000000000000000000000000000010000000000000000000000100000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000100000010100001000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000010000010000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100010010001000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000000000001000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000100000010000000000000000000000000000000000000000000000000000000000000000000001001000000": true, + "100000000000000000000000010000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000001001000000000000000000000000000000000000000000000000001001000000": true, + "1000000000000000000000000000000000000000000100000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000100001000000000000000000000000000000000000000000000000100001000000000": true, + "100000000000000000000000000000000100000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000100000000000000000000010000001000000000000000000000000000000000000000000000000000001010000000000": true, + "1000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000101000000000000000000000000000000000000000000000000000101000000000000": true, + "1000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000010001000000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000010000000000000000000000000000000100000000100000000000000000000000000000000000000000000000001000000000000000010000000000000000000000000000000100000000000000000010001": false, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000010000000100000000000000000000000": false, + "100100000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000001010000000000000": true, + "100000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000001000000001000000000000000000000000000000000000000000001000000001000000": true, + "1000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000010100000000000": false, + "1000000000000000100000000000000000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000100000000000000000000010010000000000000000000000000000000000000000000000000000010100000000000": true, + "1000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000100000000000000000000000000000000000000000000000000000000000000000": true, + "101000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000010000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000000000000100000000000010000000100000000000000000000000000110000000000000000": true, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000010000000100000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100001000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000000000100000000000000000010000000000000000010000000000000000000000000000101000": false, + "10100000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000010000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000100000000000000000000100000111000000000000000000000000000000000000000000": true, + "1000000000000000000000000000010000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000001100100100000000000000000000000000000000000000000000000000": true, + "10000000000000000000000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000100000000000000000000100000100000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001000000000000000000100000000000000000000000000000010000010000000000000000000000000000000000000": true, + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000001000000010000000000000000000000000000000000": false, + "10000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000010010000000000000000000000000000000000000": true, + "1000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000100100000000000000000000000000": true, + "1000101010000000000000000000000000000000000000000000010000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000001100000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "1010101000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000110000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "100000010000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "1001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "100000000000000000000000000000000010000000000000000000000100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000010000000000001001000000000000000000000000000000000000000000000000000000000000": true, + "1000000000000100000000000000000000000000000000001000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010000001010000000000000000000000000000000000000000000000000000000000000000000000000000": true, + "1000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000010000000000000000000000000100000000000000000000000000000000000000010000000000000000000000000000000000000000000010000010000": true, + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000010000000001000000000000000000": false + } +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg new file mode 100644 index 00000000..c4f34c0f --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Profile.json b/src/components/Projects/CircleArt/Gallery/configurations/Profile.json new file mode 100644 index 00000000..dbaa7cdc --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Profile.json @@ -0,0 +1,92 @@ +{ + "height": 774, + "width": 1584, + "circles": [ + { + "id": "5b6976c5-c033-4198-a0cd-ba52e6115486", + "radius": 202.6121489447264, + "x": 612.69, + "y": 268.32000000000005 + }, + { + "id": "ef55cc32-8b9a-4ae1-ae57-256a5d08e847", + "radius": 185.88985044913022, + "x": 901.65, + "y": 531.48 + }, + { + "id": "9cb95fba-9e6d-4404-8624-d7e86192ae56", + "radius": 143.33519665455518, + "x": 714.5999999999999, + "y": 272.19000000000005 + }, + { + "id": "ec794f04-f69b-4d6d-a297-0f74d2fb7edc", + "radius": 138.45732916678696, + "x": 915.84, + "y": 583.08 + }, + { + "id": "652750a2-66e0-43f4-9e7e-14d096fd5e0d", + "radius": 131.80745047227035, + "x": 859.0799999999999, + "y": 322.5 + }, + { + "id": "a3e8d1e6-dfe2-4694-ac67-2bfc536a567e", + "radius": 74.46328759865496, + "x": 919.7100000000002, + "y": 199.95000000000002 + }, + { + "id": "2888d28b-b885-49b6-a512-765ffd4aa86b", + "radius": 22.081243171524562, + "x": 951.9599999999999, + "y": 384.42 + }, + { + "id": "100b74b6-1606-421b-a2a2-ed7b6b3ac687", + "radius": 21.93, + "x": 922.29, + "y": 415.37999999999994 + }, + { + "id": "bcb98145-0ef4-4c03-801f-0f9cf8d626ce", + "radius": 20.191693836823102, + "x": 927.45, + "y": 313.47 + }, + { + "id": "32269cf9-1fc1-41c5-901a-f97ec7c8f58b", + "radius": 19.392952328101053, + "x": 910.68, + "y": 341.84999999999997 + }, + { + "id": "a77ddd12-089c-4ef4-abef-4669636e420a", + "radius": 15.53365700664206, + "x": 904.2299999999999, + "y": 287.67 + }, + { + "id": "1ea10506-1b14-46a8-8ca3-96af52d27168", + "radius": 13.467995396494613, + "x": 922.29, + "y": 371.52 + } + ], + "fills": { + "10000000000000000010000000000000100000000000000000000000010000000001010": true, + "100000010100000000000010100010000010001000000000000000000000001111000000111100": true, + "100000001000000001000100000000000000001010000010000000001000101000000010000100100010000110000101000110000": true, + "1000010000000000000000000000000000000000000110000": true, + "1000000001000000000000000100000000000000000000000100000001001000000": true, + "100010000010000000000000010000000000000000000010000000000100000000001001000001000000000100100000010000": true, + "101000010000000000000000001000000000000000000000000000000000001100110000000000000000000000": true, + "10100000000100000000001000000000000000000000000000000000000000000000000110000110000000000000000000000000000": true, + "1010000001000000000000000000000000000000000000000000001000000000000011000000000000000000110000000000": true, + "1000000001010000000000000000000000000000000000000000000000001000000000110000000000000000000000110000000000": true, + "10100000000100000000000000001000000000000000000000000000000000000000001100000011000000000000000000000000": true, + "100000010000000000000000000000000000000000000000110000000000000000": true + } +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg b/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg new file mode 100644 index 00000000..c1b0e331 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Whale.json b/src/components/Projects/CircleArt/Gallery/configurations/Whale.json new file mode 100644 index 00000000..083559a7 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Whale.json @@ -0,0 +1,106 @@ +{ + "height": 774, + "width": 1584, + "circles": [ + { + "id": "85f64b2b-8189-478d-a418-028a8d972c66", + "radius": 576.5362004245701, + "x": 812.64, + "y": -233.49000000000012 + }, + { + "id": "a43eaa89-1589-487b-a137-c572187e6719", + "radius": 225.16320880641223, + "x": 1079.67, + "y": 546.96 + }, + { + "id": "30e60e5a-0c3c-4bff-847a-b43f000728dc", + "radius": 223.7062343789283, + "x": 838.4399999999999, + "y": 261.87 + }, + { + "id": "d8faef6c-2f68-4e4c-9911-752b859536c1", + "radius": 207.4334440248245, + "x": 1077.09, + "y": 535.35 + }, + { + "id": "53cca615-adf1-43d1-b9b5-fb0f338aa34b", + "radius": 107.93685237211618, + "x": 583.02, + "y": 263.16 + }, + { + "id": "058af930-b6ab-4041-babe-578e35135ad8", + "radius": 82.73115072329166, + "x": 762.3299999999999, + "y": 439.89000000000004 + }, + { + "id": "2dcd8fe8-f859-47d4-85ea-fd2d429fdeb7", + "radius": 82.73115072329166, + "x": 702.99, + "y": 450.21 + }, + { + "id": "959a5108-68c9-45e8-83ac-2c9ba170c3d4", + "radius": 51.35755543247751, + "x": 672.03, + "y": 294.12 + }, + { + "id": "ad43a593-e521-4876-8000-5b0de7cd9588", + "radius": 51.35755543247751, + "x": 598.5, + "y": 356.03999999999996 + }, + { + "id": "49e18223-1742-49b5-9c67-6466f380762f", + "radius": 35.93524175513503, + "x": 639.78, + "y": 215.42999999999998 + }, + { + "id": "931647b1-ddea-4ad2-aa23-2b95de9ec1d2", + "radius": 34.85388070215424, + "x": 526.26, + "y": 312.18 + }, + { + "id": "bb1b9b24-2f61-42b7-88e2-a5fe5ceaae84", + "radius": 25.442776971077663, + "x": 1025.49, + "y": 326.37000000000006 + }, + { + "id": "1379d512-bfce-4745-b282-66f7a136704e", + "radius": 5.473006486383879, + "x": 896.49, + "y": 380.54999999999995 + } + ], + "fills": { + "1000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001100000000000000000000000000": true, + "10100000000000000000000000000000000000000000000000000000000000000000000000010000000001000000000000000000000000000000011001100000000": true, + "100000000000010000000000000000101001000000000000010000000000001000100000001000000010000000001000010010000100110000010100100100000": true, + "1000000000000000000000000000000000000000000000000010100000000000001000000000000000000000000000000001100000000001100000000000000": true, + "10001000000000000000000000000000001000000000000000000000000001000000000000001010000000000000000000": true, + "10000100000000000000000000000000010000000000000000000000000001000000000000001001000000000000000000": true, + "1000100000000000000000000000000100000000000000000000000000001000000000000000101000000000000000000": true, + "10000000000000000000000000000000000100000000000010000000000000000000000000000000000010000000100000100000000000000000": true, + "100000000000000000001000000000000000000000000000000100000000000000000010000000000000000000000101000": true, + "100000000000101000000000000000000001000000000000000000000000000001000000001100000000010000000000000000000000011000": true, + "100000000000010000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000": true, + "1010000000000010000000000000000000100000000000100000000000000000100000000001100000010000000000000100000000010000001": true, + "1000000000000001000000000000000000010000000000000000000000000000000000000001000000010010000000000000000000000000000": true, + "100000000000010100000000000000000000000001000000000000100000000000000001000000011000000000001000000000000100000000001000001": true, + "100000000000101000000000000100000000000000000000000000000000000000001000000110000000000100000000000000000000000011000": true, + "10001000000000000000000000000001000000000000000000000000010000000000000001010000000000000000000": true, + "100000000000010000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000": true, + "100000000000001000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000": true, + "10000000000000000100000000000000010000000000000000000000000000000000000000000000010000000100100000000000000000000000000000": true, + "1000000000000000100000000000000000000000010000000000000000000000000000000000000010000000100001000000000000000000000000000000": true + } +} diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg b/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg new file mode 100644 index 00000000..b570c5e8 --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Projects/CircleArt/Gallery/configurations/index.ts b/src/components/Projects/CircleArt/Gallery/configurations/index.ts new file mode 100644 index 00000000..e09afd6a --- /dev/null +++ b/src/components/Projects/CircleArt/Gallery/configurations/index.ts @@ -0,0 +1,35 @@ +import { CircleArtGalleryItem } from "../../types"; + +const data: CircleArtGalleryItem[] = [{ + author: 'Dorota Pankoska', + authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html', + config: require('./Fox.json'), + name: 'Fox', + thumbnail: require('./Fox.svg'), +}, { + author: 'Dorota Pankoska', + authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html', + config: require('./Monkey.json'), + name: 'Monkey', + thumbnail: require('./Monkey.svg'), +}, { + author: 'Dorota Pankoska', + authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html', + config: require('./Whale.json'), + name: 'Whale', + thumbnail: require('./Whale.svg'), +}, { + author: 'Unknown', + authorUrl: 'https://www.reddit.com/r/Damnthatsinteresting/comments/963j4n/magic_of_circles', + config: require('./Profile.json'), + name: 'Profile', + thumbnail: require('./Profile.svg'), +}, { + author: 'Me', + authorUrl: 'https://hogg.io', + config: require('./Mushroom.json'), + name: 'Mushroom', + thumbnail: require('./Mushroom.svg'), +}]; + +export default data; diff --git a/src/components/Projects/CircleArt/types.ts b/src/components/Projects/CircleArt/types.ts new file mode 100644 index 00000000..40bfca30 --- /dev/null +++ b/src/components/Projects/CircleArt/types.ts @@ -0,0 +1,16 @@ +import { Circle } from "../IntersectionExplorer/useGraph"; + +export type CircleArtData = { + width: number; + height: number; + circles: Circle[]; + fills: Record; +}; + +export type CircleArtGalleryItem = { + author: string; + authorUrl: string; + config: CircleArtData; + name: string; + thumbnail: string; +}; diff --git a/src/components/Projects/CircleArt/utils/cursor.ts b/src/components/Projects/CircleArt/utils/cursor.ts new file mode 100644 index 00000000..4bded9d5 --- /dev/null +++ b/src/components/Projects/CircleArt/utils/cursor.ts @@ -0,0 +1,40 @@ +import atan2 from "./math/atan2"; + +type CircleEditorCursor = + | 'default' + | 'crosshair' + | 'pointer' + | 'move' + | 'ns-resize' + | 'nesw-resize' + | 'ew-resize' + | 'nwse-resize'; + + +export const CURSOR_DEFAULT: CircleEditorCursor = 'default'; +export const CURSOR_DRAW: CircleEditorCursor = 'crosshair'; +export const CURSOR_FILL: CircleEditorCursor = 'pointer'; +export const CURSOR_MOVE: CircleEditorCursor = 'move'; +export const CURSOR_RESIZE_T_B: CircleEditorCursor = 'ns-resize'; +export const CURSOR_RESIZE_BL_TR: CircleEditorCursor = 'nesw-resize'; +export const CURSOR_RESIZE_L_R: CircleEditorCursor = 'ew-resize'; +export const CURSOR_RESIZE_BR_TL: CircleEditorCursor = 'nwse-resize'; + +export const getCursor = (px: number, py: number, cx: number, cy: number) => { + const a = (atan2(px, py, cx, cy) * 180) / Math.PI; + + if (a > 247.5 && a < 292.5) return CURSOR_RESIZE_T_B; + if (a > 292.5 && a < 337.5) return CURSOR_RESIZE_BL_TR; + if (a > 337.5 || a < 22.5) return CURSOR_RESIZE_L_R; + if (a > 22.5 && a < 67.5) return CURSOR_RESIZE_BR_TL; + if (a > 67.5 && a < 112.5) return CURSOR_RESIZE_T_B; + if (a > 112.5 && a < 157.5) return CURSOR_RESIZE_BL_TR; + if (a > 157.5 && a < 202.5) return CURSOR_RESIZE_L_R; + if (a > 202.5 && a < 247.5) return CURSOR_RESIZE_BR_TL; + + return CURSOR_DRAW; +}; + +export const setCursor = (element: null | HTMLElement | SVGElement, cursor: CircleEditorCursor) => { + if (element) element.style.cursor = cursor; +}; diff --git a/src/components/Projects/CircleArt/utils/math/atan2.ts b/src/components/Projects/CircleArt/utils/math/atan2.ts new file mode 100644 index 00000000..2085c4c3 --- /dev/null +++ b/src/components/Projects/CircleArt/utils/math/atan2.ts @@ -0,0 +1,5 @@ +export default (x1: number, y1: number, x2: number, y2: number, normalise = true) => { + let a = Math.atan2(y1 - y2, x1 - x2); + if (normalise && a < 0) a += Math.PI * 2; + return a; +}; diff --git a/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts b/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts new file mode 100644 index 00000000..e85d21c1 --- /dev/null +++ b/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts @@ -0,0 +1,5 @@ +import isPointWithinCircle from './isPointWithinCircle'; + +export default (px: number, py: number, cx: number, cy: number, radius: number, padding: number = 0) => + isPointWithinCircle(px, py, cx, cy, radius, padding) && + !isPointWithinCircle(px, py, cx, cy, radius, padding * -1); diff --git a/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts b/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts new file mode 100644 index 00000000..8dfb26b1 --- /dev/null +++ b/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts @@ -0,0 +1,3 @@ +export default (px: number, py: number, cx: number, cy: number, radius: number, padding: number = 0) => + ((px - cx) ** 2) + ((py - cy) ** 2) < ((radius + padding) ** 2); + diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx b/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx index bcaac434..24d8a754 100644 --- a/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx +++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx @@ -3,107 +3,38 @@ import { motion } from 'framer-motion'; import { Box, useResizeObserver } from 'preshape'; import React, { useContext, useMemo, useRef, PointerEvent } from 'react'; import { IntersectionExplorerContext } from '../IntersectionExplorer'; -import { Edge, Node, Traversal } from '../useGraph'; import GraphVisualisationEdge from './GraphVisualisationEdge'; import GraphVisualisationLabel from './GraphVisualisationLabel'; import GraphVisualisationNode from './GraphVisualisationNode'; import GraphVisualisationTraversal from './GraphVisualisationTraversal'; +import getArcPath from './getArcPath'; +import getScaledProps from './getScaledProps'; +import getTraversalPath from './getTraversalPath'; import useLabelPositionShifts from './useLabelPositionShifts'; import './GraphVisualisation.css'; -const scale = (v: number, m: number) => m * (v / 1); - -// eslint-disable-next-line @typescript-eslint/ban-types -const scaleProps = ( - entities: T[], - props: (keyof T)[], - range: number -): T[] => { - return entities.map((entity) => { - const entityScaled: T = { ...entity }; - - for (const prop of props) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - entityScaled[prop] = scale( - entityScaled[prop] as unknown as number, - range - ) as any; - } - - return entityScaled; - }); -}; - -const getArcPath = ( - edge: Edge, - nodes: Node[], - start = true, - reverse = false -): string => { - const { - angleStart, - angleEnd, - nodes: [a, b], - radius, - } = edge; - const { x: sx, y: sy } = reverse ? nodes[b] : nodes[a]; - const { x: ex, y: ey } = reverse ? nodes[a] : nodes[b]; - - const largeArcFlag = Math.abs(angleEnd - angleStart) >= Math.PI ? 1 : 0; - const sweepFlag = reverse ? 0 : 1; - - return ( - (start ? `M ${sx} ${sy} ` : '') + - `A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${ex} ${ey} ` - ); -}; - -const getTraversalPath = ( - traversal: Traversal, - nodes: Node[], - edges: Edge[] -): string => { - let path = ''; - - for (let i = 1; i < traversal.path.length; i += 2) { - const e = traversal.path[i] - nodes.length; - const a = traversal.path[i - 1]; - const reverse = edges[e].nodes[0] !== a; - - path += - getArcPath( - edges[traversal.path[i] - nodes.length], - nodes, - i === 1, - reverse - ) + ' '; - } - - return path; -}; - interface Props { onNodeOver: (index: number) => void; onTraversalOver: (index: number) => void; } const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => { - const { activeNodeIndex, addToTraversal, graph, traversals } = useContext( + const { activeNodeIndex, addToTraversal, graph } = useContext( IntersectionExplorerContext ); const [size, ref] = useResizeObserver(); const min = size.width; const circles = useMemo( - () => scaleProps(graph.circles, ['radius', 'x', 'y'], min), + () => getScaledProps(graph.circles, ['radius', 'x', 'y'], min), [min] ); const edges = useMemo( - () => scaleProps(graph.edges, ['radius', 'x', 'y'], min), + () => getScaledProps(graph.edges, ['radius', 'x', 'y'], min), [graph, min] ); const nodes = useMemo( - () => scaleProps(graph.nodes, ['x', 'y'], min), + () => getScaledProps(graph.nodes, ['x', 'y'], min), [graph, min] ); const classes = classNames('GraphVisualisation', { @@ -113,7 +44,7 @@ const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => { const refLabels = useRef(null); const refObstacles = useRef(null); const labelPositionShifts = useLabelPositionShifts( - { circles, edges, nodes }, + { ...graph, circles, edges, nodes }, refLabels.current, refObstacles.current ); @@ -155,7 +86,7 @@ const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => { ))} {/* Animating yellow traversals */} - {traversals.map((traversal, index) => ( + {graph.traversals.map((traversal, index) => ( { const { d, index, onPointerOver, traversal } = props; - const { activeNodeIndex, activeTraversalIndex, traversals } = useContext( + const { activeNodeIndex, activeTraversalIndex, graph } = useContext( IntersectionExplorerContext ); - const currentTraversal = getCurrentTraversal(traversals); + const currentTraversal = getCurrentTraversal(graph.traversals); const refGlow = useRef(null); const refPath = useRef(null); diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts new file mode 100644 index 00000000..f6fa2ea5 --- /dev/null +++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts @@ -0,0 +1,27 @@ +import { Edge, Node } from "../useGraph"; + +const getArcPath = ( + edge: Edge, + nodes: Node[], + start = true, + reverse = false +): string => { + const { + angleStart, + angleEnd, + nodes: [a, b], + radius, + } = edge; + const { x: sx, y: sy } = reverse ? nodes[b] : nodes[a]; + const { x: ex, y: ey } = reverse ? nodes[a] : nodes[b]; + + const largeArcFlag = Math.abs(angleEnd - angleStart) >= Math.PI ? 1 : 0; + const sweepFlag = reverse ? 0 : 1; + + return ( + (start ? `M ${sx} ${sy} ` : '') + + `A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${ex} ${ey} ` + ); +}; + +export default getArcPath; diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts new file mode 100644 index 00000000..5cd77e0f --- /dev/null +++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts @@ -0,0 +1,22 @@ +export const scale = (v: number, m: number) => m * (v / 1); + +const getScaledProps = ( + entities: T[], + props: (keyof T)[], + range: number +): T[] => { + return entities.map((entity) => { + const entityScaled: T = { ...entity }; + + for (const prop of props) { + entityScaled[prop] = scale( + entityScaled[prop] as unknown as number, + range + ) as any; + } + + return entityScaled; + }); +}; + +export default getScaledProps; diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts new file mode 100644 index 00000000..4987819f --- /dev/null +++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts @@ -0,0 +1,28 @@ +import { Traversal, Edge, Node } from "../useGraph"; +import getArcPath from "./getArcPath"; + +const getTraversalPath = ( + traversal: Traversal, + nodes: Node[], + edges: Edge[] +): string => { + let path = ''; + + for (let i = 1; i < traversal.path.length; i += 2) { + const e = traversal.path[i] - nodes.length; + const a = traversal.path[i - 1]; + const reverse = edges[e].nodes[0] !== a; + + path += + getArcPath( + edges[traversal.path[i] - nodes.length], + nodes, + i === 1, + reverse + ) + ' '; + } + + return path; +}; + +export default getTraversalPath; diff --git a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css index 18d861e1..9535cc08 100644 --- a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css +++ b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css @@ -11,11 +11,12 @@ } .IntersectionExplorer--120 { - grid-template-columns: 1fr 1fr; + grid-template-columns: 300px 1fr; + grid-template-rows: min-content 1fr; & > :nth-child(1) { grid-column: 1; grid-row: 1; } - & > :nth-child(2) { grid-column: 2; grid-row: 1; } - & > :nth-child(3) { grid-column: span 2; grid-row: 2; } + & > :nth-child(2) { grid-column: 2; grid-row: span 2; } + & > :nth-child(3) { grid-column: 1; grid-row: 2; } } .IntersectionExplorer--111 { diff --git a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx index 809ebbea..38556d46 100644 --- a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx +++ b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import { Box, useMatchMedia } from 'preshape'; +import { Box, Text, useMatchMedia } from 'preshape'; import React, { createContext, useEffect, useRef, useState } from 'react'; import GraphVisualisation from './GraphVisualisation/GraphVisualisation'; import NodeList from './NodeList/NodeList'; @@ -36,8 +36,8 @@ export const IntersectionExplorerContext = createContext({ circles: [], edges: [], nodes: [], + traversals: [], }, - traversals: [], }); const IntersectionExplorer = ({ @@ -105,6 +105,10 @@ const IntersectionExplorer = ({ ref={refContainer} > + + Nodes & Edges + + setActiveNodeIndex(i)} /> @@ -116,6 +120,10 @@ const IntersectionExplorer = ({ + + Traversals + + setActiveTraversalIndex(i)} /> diff --git a/src/components/Projects/IntersectionExplorer/NodeList/NodeList.css b/src/components/Projects/IntersectionExplorer/NodeList/NodeList.css index c2c940a4..46865f1f 100644 --- a/src/components/Projects/IntersectionExplorer/NodeList/NodeList.css +++ b/src/components/Projects/IntersectionExplorer/NodeList/NodeList.css @@ -1,7 +1,7 @@ .NodeList__item { width: 100%; - background-color: var(--color-text-shade-1); - color: var(--color-background-shade-1); + background-color: var(--color-dark-shade-1); + color: var(--color-light-shade-1); transition-property: transform, width; transition-duration: var(--transition-duration--base); transition-timing-function: var(--transition-timing-function); diff --git a/src/components/Projects/IntersectionExplorer/NodeList/NodeList.tsx b/src/components/Projects/IntersectionExplorer/NodeList/NodeList.tsx index 47acb383..a61b6659 100644 --- a/src/components/Projects/IntersectionExplorer/NodeList/NodeList.tsx +++ b/src/components/Projects/IntersectionExplorer/NodeList/NodeList.tsx @@ -12,14 +12,9 @@ interface Props { } const NodeList = ({ onNodeOver }: Props) => { - const { - activeNodeIndex, - addToTraversal, - cancelTraversal, - graph, - traversals, - } = useContext(IntersectionExplorerContext); - const currentTraversal = getCurrentTraversal(traversals); + const { activeNodeIndex, addToTraversal, cancelTraversal, graph } = + useContext(IntersectionExplorerContext); + const currentTraversal = getCurrentTraversal(graph.traversals); const currentTraversalNode = currentTraversal?.path[currentTraversal.path.length - 1]; const nodesSorted = getSortedNodes(graph, currentTraversal); diff --git a/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltip.tsx b/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltip.tsx index debd6d61..31987ccf 100644 --- a/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltip.tsx +++ b/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltip.tsx @@ -61,16 +61,16 @@ const NodeTooltip: FunctionComponent = (props) => { unrender visible={visible} > - + event.stopPropagation()} paddingHorizontal="x4" paddingVertical="x4" style={{ pointerEvents: 'none' }} - textColor="background-shade-1" + textColor="light-shade-1" > diff --git a/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltipContentNext.tsx b/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltipContentNext.tsx index 53296a2a..9bf32921 100644 --- a/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltipContentNext.tsx +++ b/src/components/Projects/IntersectionExplorer/NodeTooltip/NodeTooltipContentNext.tsx @@ -24,7 +24,7 @@ const NodeTooltipContentNext: FunctionComponent = (props) => { {validations.map((validation) => ( = ({ onTraversalOver }) => { - const { traversals } = useContext(IntersectionExplorerContext); - const completeTraversals = getCompleteTraversals(traversals); + const { graph } = useContext(IntersectionExplorerContext); + const completeTraversals = getCompleteTraversals(graph.traversals); + + if (completeTraversals.length === 0) { + return No traversals added; + } return ( diff --git a/src/components/Projects/IntersectionExplorer/useGraph/circle.ts b/src/components/Projects/IntersectionExplorer/useGraph/circle.ts index a2b295e6..136d8b33 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/circle.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/circle.ts @@ -1,6 +1,5 @@ -import floor from 'lodash.floor'; - export interface Circle { + id?: string; radius: number; x: number; y: number; @@ -40,8 +39,8 @@ export const getIntersectionPoints = ( const y = y1 + (a * (y2 - y1)) / d; const rx = -(y2 - y1) * (h / d); const ry = -(x2 - x1) * (h / d); - const p1: [number, number] = [floor(x + rx, 5), floor(y - ry, 5)]; - const p2: [number, number] = [floor(x - rx, 5), floor(y + ry, 5)]; + const p1: [number, number] = [x + rx, y - ry]; + const p2: [number, number] = [x - rx, y + ry]; return [p1, p2]; } diff --git a/src/components/Projects/IntersectionExplorer/useGraph/edge.ts b/src/components/Projects/IntersectionExplorer/useGraph/edge.ts index 2c8c72d4..44749e83 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/edge.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/edge.ts @@ -1,8 +1,8 @@ import Bitset from 'bitset'; import { Circle } from './circle'; +import { GraphContext } from './graph'; import { Node, NodeState } from './node'; import { validateEdge } from './validate'; -import { GraphContext } from '.'; export type Edge = { angleStart: number; diff --git a/src/components/Projects/IntersectionExplorer/useGraph/graph.ts b/src/components/Projects/IntersectionExplorer/useGraph/graph.ts new file mode 100644 index 00000000..4921127f --- /dev/null +++ b/src/components/Projects/IntersectionExplorer/useGraph/graph.ts @@ -0,0 +1,57 @@ + +import { Circle } from "./circle"; +import { Edge, getEdgeState } from "./edge"; +import { getNodeState, Node, NodeState } from "./node"; +import { getCurrentTraversal, getCompleteTraversals, Traversal } from "./traversal"; + +export interface Graph { + circles: Circle[]; + edges: Edge[]; + nodes: Node[]; + traversals: Traversal[]; +} + +export interface GraphState { + edges: NodeState[]; + nodes: NodeState[]; +} + +export interface GraphContext { + circles: Circle[]; + edges: Edge[]; + nodes: Node[]; + traversals: Traversal[]; + traversalCurrent: Traversal | null; + traversalCurrentNode: number | null; + traversalsComplete: Traversal[]; +} + +/** + * + */ + export const getUpdatedGraphState = (graph: Graph): Graph => { + const traversalCurrent = getCurrentTraversal(graph.traversals); + const traversalCurrentNode = + traversalCurrent && + traversalCurrent.path[traversalCurrent.path.length - 1]; + const traversalsComplete = getCompleteTraversals(graph.traversals); + + const context: GraphContext = { + ...graph, + traversalCurrent, + traversalCurrentNode, + traversalsComplete, + }; + + return { + ...graph, + edges: graph.edges.map((edge) => ({ + ...edge, + state: getEdgeState(edge, context), + })), + nodes: graph.nodes.map((node) => ({ + ...node, + state: getNodeState(node, context), + })), + }; +}; diff --git a/src/components/Projects/IntersectionExplorer/useGraph/index.ts b/src/components/Projects/IntersectionExplorer/useGraph/index.ts index e022f791..3542df56 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/index.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/index.ts @@ -1,38 +1,16 @@ import { useEffect, useState } from 'react'; import { Circle } from './circle'; -import { Edge, getEdges, getEdgeState } from './edge'; -import { Node, NodeState, getNodes, getNodeState } from './node'; +import { Edge, getEdges } from './edge'; +import { Graph, getUpdatedGraphState } from './graph'; +import { Node, NodeState, getNodes } from './node'; import { Traversal, - appendEdgeToPath, - getNewTraversal, - getCurrentTraversal, - getCompleteTraversals, + addIndexToTraversal, + getTraversals, } from './traversal'; import { ValidationRuleResult } from './validate'; -export { Circle, Edge, Node, NodeState, Traversal, ValidationRuleResult }; - -export interface Graph { - circles: Circle[]; - edges: Edge[]; - nodes: Node[]; -} - -export interface GraphState { - edges: NodeState[]; - nodes: NodeState[]; -} - -export interface GraphContext { - circles: Circle[]; - edges: Edge[]; - nodes: Node[]; - traversals: Traversal[]; - traversalCurrent: Traversal | null; - traversalCurrentNode: number | null; - traversalsComplete: Traversal[]; -} +export { Circle, Graph, Edge, Node, NodeState, Traversal, ValidationRuleResult }; export interface HookResult { /** @@ -49,10 +27,6 @@ export interface HookResult { * */ cancelTraversal: () => void; - /** - * - */ - graph: Graph; /** * */ @@ -60,11 +34,13 @@ export interface HookResult { /** * */ - traversals: Traversal[]; + graph: Graph; } -/** Consistent referenced array */ -const DONT_REMOVE__READ_COMMENT: [] = []; +type UseGraphOptions = { + findTraversalsOnUpdate?: boolean; + traversals?: Traversal[]; +}; /** * @@ -73,46 +49,43 @@ const DONT_REMOVE__READ_COMMENT: [] = []; */ export default function useGraph( circles: Circle[], - traversalsControlled: Traversal[] = DONT_REMOVE__READ_COMMENT + opts: UseGraphOptions = {}, ): HookResult { - const [traversals, setTraversals] = useState([]); + const { + findTraversalsOnUpdate = false, + traversals: traversalsControlled, + } = opts; + const [graph, setGraph] = useState({ - circles: circles, + circles: [], edges: [], nodes: [], + traversals: [], }); - const addToTraversal = (point: number) => { - const currentTraversal = getCurrentTraversal(traversals); - - if (currentTraversal) { - if (point < graph.nodes.length) { - throw new Error( - 'Once a traversal has been started, only edges can be added.' - ); - } - - setTraversals([ - ...traversals.slice(0, -1), - appendEdgeToPath( - currentTraversal, - graph.edges[point - graph.nodes.length] - ), - ]); - } else { - setTraversals([...traversals, getNewTraversal(traversals.length, point)]); - } + const addToTraversal = (index: number) => { + setGraph((graph) => { + const traversals = addIndexToTraversal(graph, index); + return getUpdatedGraphState({ ...graph, traversals }); + }); }; const removeTraversal = (index: number) => { - setTraversals((traversals) => [ - ...traversals.slice(0, index), - ...traversals.slice(index + 1), - ]); + setGraph((graph) => { + const traversals = [ + ...graph.traversals.slice(0, index), + ...graph.traversals.slice(index + 1), + ]; + + return getUpdatedGraphState({ + ...graph, + traversals, + }); + }); }; const cancelTraversal = () => { - removeTraversal(traversals.length - 1); + removeTraversal(graph.traversals.length - 1); }; /** @@ -121,43 +94,19 @@ export default function useGraph( useEffect(() => { const nodes = getNodes(circles); const edges = getEdges(circles, nodes); + const traversals = findTraversalsOnUpdate ? getTraversals(circles, nodes, edges) : []; + const graph = getUpdatedGraphState({ circles, nodes, edges, traversals }); - setGraph({ circles, nodes, edges }); - }, [circles]); + setGraph(graph); + }, [circles, findTraversalsOnUpdate]); - /** - * On traversals changing, update the graph state. - */ useEffect(() => { - const traversalCurrent = getCurrentTraversal(traversals); - const traversalCurrentNode = - traversalCurrent && - traversalCurrent.path[traversalCurrent.path.length - 1]; - const traversalsComplete = getCompleteTraversals(traversals); - - const context: GraphContext = { - ...graph, - traversalCurrent, - traversalCurrentNode, - traversals, - traversalsComplete, - }; - - setGraph((graph) => ({ - ...graph, - edges: graph.edges.map((edge) => ({ - ...edge, - state: getEdgeState(edge, context), - })), - nodes: graph.nodes.map((node) => ({ - ...node, - state: getNodeState(node, context), - })), - })); - }, [traversals]); - - useEffect(() => { - setTraversals(traversalsControlled); + if (traversalsControlled) { + setGraph((graph) => getUpdatedGraphState({ + ...graph, + traversals: traversalsControlled, + })); + } }, [traversalsControlled]); return { @@ -165,6 +114,5 @@ export default function useGraph( cancelTraversal, removeTraversal, graph, - traversals, }; } diff --git a/src/components/Projects/IntersectionExplorer/useGraph/node.ts b/src/components/Projects/IntersectionExplorer/useGraph/node.ts index ac65d7ff..189bd369 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/node.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/node.ts @@ -1,8 +1,8 @@ import Bitset from 'bitset'; import { Circle, atan2, getIntersectionPoints } from './circle'; import { Edge } from './edge'; +import { GraphContext } from './graph'; import { ValidationRuleResult, Validations } from './validate'; -import { GraphContext } from '.'; export type Node = { [n: number]: number; diff --git a/src/components/Projects/IntersectionExplorer/useGraph/traversal.ts b/src/components/Projects/IntersectionExplorer/useGraph/traversal.ts index 6416aaf8..4e82e49a 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/traversal.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/traversal.ts @@ -1,5 +1,9 @@ import Bitset from 'bitset'; +import isPointWithinCircle from '../../CircleArt/utils/math/isPointWithinCircle'; +import { Circle } from './circle'; import { Edge } from './edge'; +import { getUpdatedGraphState, Graph } from './graph'; +import { Node } from './node'; export interface Traversal { /** @@ -36,6 +40,31 @@ export const getNewTraversal = (index: number, node?: number): Traversal => ({ path: node === undefined ? [] : [node], }); +/** + * + */ +export const addIndexToTraversal = ({ traversals, nodes, edges }: Graph, index: number) => { + const currentTraversal = getCurrentTraversal(traversals); + + if (currentTraversal) { + if (index < nodes.length) { + throw new Error( + 'Once a traversal has been started, only edges can be added.' + ); + } + + return [ + ...traversals.slice(0, -1), + appendEdgeToPath( + currentTraversal, + edges[index - nodes.length] + ), + ]; + } else { + return [...traversals, getNewTraversal(traversals.length, index)]; + } +}; + /** * @param {Traversal} traversal The traversal to add the point to * @param {Edge} edge The edge to be added to the traversal. @@ -80,3 +109,95 @@ export const getCurrentTraversal = ( export const getCompleteTraversals = (traversals: Traversal[]): Traversal[] => { return traversals.filter(({ isComplete }) => isComplete); }; + +/** + * + */ +export const removeTraversal = (traversals: Traversal[], index: number) => { + return [ + ...traversals.slice(0, index), + ...traversals.slice(index + 1), + ]; +}; + +/** + * + */ +export const cancelCurrentTraversal = (traversals: Traversal[]) => { + return removeTraversal(traversals, traversals.length - 1); +}; + +/** + * + */ +export const getTraversals = (circles: Circle[], nodes: Node[], edges: Edge[]): Traversal[] => { + let graph: Graph = getUpdatedGraphState({ circles, nodes, edges, traversals: [] }); + + const links: Record = {}; + + for (const edge of graph.edges) { + for (const nodeIndex of edge.nodes) { + links[nodeIndex] = links[nodeIndex] || []; + links[nodeIndex].push(edge.index); + } + } + + const traverseEdges = (fromNodeIndex: number) => { + let hasPath = false; + + for (const edgeIndex of links[fromNodeIndex]) { + if (!getCurrentTraversal(graph.traversals)) { + graph = getUpdatedGraphState({ + ...graph, + traversals: addIndexToTraversal(graph, fromNodeIndex), + }); + } + + if (graph.edges[edgeIndex- graph.nodes.length].state.isSelectable) { + hasPath = true; + + graph = getUpdatedGraphState({ + ...graph, + traversals: addIndexToTraversal(graph, edgeIndex), + }); + + const currentTraversal = getCurrentTraversal(graph.traversals); + + if (currentTraversal && !currentTraversal.isComplete) { + traverseEdges(currentTraversal.path[currentTraversal.path.length - 1]); + } + } + } + + if (!hasPath) { + graph = getUpdatedGraphState({ + ...graph, + traversals: cancelCurrentTraversal(graph.traversals), + }); + } + }; + + for (const node of graph.nodes) { + if (node.state.isSelectable) { + traverseEdges(node.index); + } + } + + return graph.traversals.filter(({ path }) => { + const edges = path + .filter((index) => index >= graph.nodes.length) + .map((edgeIndex) => graph.edges[edgeIndex - graph.nodes.length]); + + for (const edge of edges) { + for (let circleIndex = 0; circleIndex < graph.circles.length; circleIndex ++) { + const { x: cx, y: cy, radius: cr } = graph.circles[circleIndex]; + + if (circleIndex !== edge.circle && isPointWithinCircle(edge.x, edge.y, cx, cy, cr)) { + return true; + } + } + } + + return false + }); +} diff --git a/src/components/Projects/IntersectionExplorer/useGraph/validate.ts b/src/components/Projects/IntersectionExplorer/useGraph/validate.ts index 22afdb27..7dfe6f6c 100644 --- a/src/components/Projects/IntersectionExplorer/useGraph/validate.ts +++ b/src/components/Projects/IntersectionExplorer/useGraph/validate.ts @@ -1,7 +1,7 @@ import { isPointInCircle } from './circle'; import { Edge, getOppositeEndNode } from './edge'; +import { GraphContext } from './graph'; import { appendEdgeToPath } from './traversal'; -import { GraphContext } from '.'; export interface ValidationRuleResult { number: 1 | 2 | 3 | 4; diff --git a/src/components/Projects/IntersectionExplorer/useTraversals/index.ts b/src/components/Projects/IntersectionExplorer/useTraversals/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Projects/Snake/Snake.tsx b/src/components/Projects/Snake/Snake.tsx new file mode 100644 index 00000000..2a3f13aa --- /dev/null +++ b/src/components/Projects/Snake/Snake.tsx @@ -0,0 +1,84 @@ +import { Box, Editor, useLocalStorage, useMatchMedia } from 'preshape'; +import React, { useMemo } from 'react'; +import data from '../../../data'; +import useEnforcedTheme from '../../../utils/useEnforcedTheme'; +import ProjectPage from '../../ProjectPage/ProjectPage'; +import SnakeControls from './SnakeControls'; +import SnakeProvider from './SnakeProvider'; +import SnakeScoreTiles from './SnakeScoreTiles'; +import SnakeViewer from './SnakeViewer'; +import { tailEscape } from './solutions'; +import { TypeSolution } from './types'; + +import 'brace/mode/javascript'; + +const Snake = () => { + const match = useMatchMedia(['1000px']); + const [editorState, setEditorState] = useLocalStorage( + 'com.hogg.snake.editor', + tailEscape + ); + + const worker = useMemo(() => { + return new Worker('./SnakeRunnerWorker.ts'); + }, []); + + useEnforcedTheme('night'); + + const onChange = (content: string) => { + setEditorState({ ...editorState, content }); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default Snake; diff --git a/src/components/Projects/Snake/SnakeControls.tsx b/src/components/Projects/Snake/SnakeControls.tsx new file mode 100644 index 00000000..7cc9a934 --- /dev/null +++ b/src/components/Projects/Snake/SnakeControls.tsx @@ -0,0 +1,139 @@ +import { + useMatchMedia, + Button, + Buttons, + Box, + Icons, + Tooltip, + BoxProps, +} from 'preshape'; +import React from 'react'; +import { useSnakeContext } from './SnakeProvider'; + +const SnakeControls = (props: BoxProps) => { + const { + history, + isStarted, + isRunning, + onPause, + onPlay, + onRefresh, + onReset, + onStart, + onStepBackwards, + onStepForwards, + } = useSnakeContext(); + + const isAtBeginning = history[0] && !history[0].path.length; + const match = useMatchMedia(['600px']); + + return ( + + + + + + + + + + + {(props) => ( + + )} + + + + {(props) => ( + + )} + + + + {(props) => ( + + )} + + + + {(props) => ( + + )} + + + + {(props) => ( + + )} + + + + + + + + + + + ); +}; + +export default SnakeControls; diff --git a/src/components/Projects/Snake/SnakeProvider.tsx b/src/components/Projects/Snake/SnakeProvider.tsx new file mode 100644 index 00000000..d9c290a3 --- /dev/null +++ b/src/components/Projects/Snake/SnakeProvider.tsx @@ -0,0 +1,251 @@ +import React, { + createContext, + FC, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import SolutionRunner from './SolutionRunner'; +import { + TypeCell, + TypeHistory, + TypePoint, + TypeSnake, + TypeValues, +} from './types'; +import getSurroundingCells from './utils/getSurroundingCells'; +import { createBlock, moveForwards, moveBackwards } from './utils/history'; +import isCellIncluded from './utils/isCellIncluded'; + +interface Props { + solution: string; + timeout?: number; + worker: Worker; + xLength?: number; + yLength?: number; +} + +export const SnakeContext = createContext<{ + history: TypeHistory; + isStarted: boolean; + isRunning: boolean; + logs: string[]; + onClearLog: () => void; + onPause: () => void; + onPlay: () => void; + onRefresh: () => void; + onReset: () => void; + onStart: () => void; + onStepBackwards: () => void; + onStepForwards: () => void; + point: undefined | TypePoint; + snake: undefined | TypeSnake; + values: undefined | TypeValues; + xLength: number; + yLength: number; +}>({ + history: [], + isStarted: false, + isRunning: false, + logs: [], + onClearLog: () => {}, + onPause: () => {}, + onPlay: () => {}, + onRefresh: () => {}, + onReset: () => {}, + onStart: () => {}, + onStepBackwards: () => {}, + onStepForwards: () => {}, + point: undefined, + snake: undefined, + values: undefined, + xLength: 15, + yLength: 15, +}); + +export const useSnakeContext = () => useContext(SnakeContext); + +const SnakeProvider: FC = (props) => { + const { + solution, + timeout = 1000, + worker, + xLength = 15, + yLength = 15, + ...rest + } = props; + + const refAnimationFrame = useRef(); + const refSolutionRunner = useRef(); + const [isStarted, setIsStarted] = useState(false); + const [isRunning, setIsRunning] = useState(false); + const [history, setHistory] = useState([]); + const [logs, setLogs] = useState([]); + const [values, setValues] = useState(); + + const level = history[history.length - 1]; + const snake = level && level.snake; + const point = level && level.point; + + const handleReset = () => { + if (refAnimationFrame.current) { + cancelAnimationFrame(refAnimationFrame.current); + } + + setIsStarted(false); + setIsRunning(false); + setValues(undefined); + setHistory([]); + }; + + const handleLog = (log: string) => { + setLogs([log, ...logs]); + }; + + const runSolution = useCallback(() => { + if (point && snake) { + refSolutionRunner.current?.run({ + fn: solution, + env: { + xMax: xLength, + yMax: yLength, + snake: snake, + point: point, + }, + }); + } else if (snake.length === xLength * yLength) { + setIsRunning(false); + handleLog('🎉 You have conquered Snake! 🎉'); + } + }, [solution, point, snake]); + + const moveSnake = useCallback(() => { + if (!Array.isArray(values)) { + setIsRunning(false); + + return handleLog( + '🔥 There were no heuristic values to calculate a move 🔥' + ); + } + + if (!snake || !point) { + setIsRunning(false); + + return handleLog('Press start!'); + } + + const cells = getSurroundingCells(xLength, yLength, snake); + let nextValue; + let nextCell: TypeCell | undefined; + + for (let i = 0; i < cells.length; i++) { + if (snake.length === xLength * yLength - 1 && cells.length === 2) { + if (isCellIncluded([cells[i]], point)) { + nextValue = values[cells[i][1]][cells[i][0]]; + nextCell = cells[i]; + } + } else { + if ( + nextValue === undefined || + values[cells[i][1]][cells[i][0]] < nextValue + ) { + nextValue = values[cells[i][1]][cells[i][0]]; + nextCell = cells[i]; + } + } + } + + if (!nextCell) { + setIsRunning(false); + + return handleLog( + 'The 🐍 did not reach the point. There were no valid cells to move to.' + ); + } + + if (isCellIncluded([nextCell], point) && snake) { + setHistory( + createBlock(xLength, yLength, moveForwards(history, nextCell, true)) + ); + } else { + setHistory(moveForwards(history, nextCell)); + } + }, [values, snake, point]); + + useEffect(() => { + refSolutionRunner.current = new SolutionRunner({ timeout, worker }); + + return () => { + refSolutionRunner.current?.destroy(); + }; + }, [timeout, worker]); + + useEffect(() => { + if (refSolutionRunner.current) { + refSolutionRunner.current.onMessage = ({ values }) => { + if (isStarted) { + setValues(values); + } + }; + + refSolutionRunner.current.onError = ({ message }) => { + setIsRunning(false); + handleLog(message); + }; + + if (!isStarted) { + refSolutionRunner.current.reset(); + } + } + }, [isStarted, isRunning]); + + useEffect(() => { + if (!point || !snake) { + setValues(undefined); + setHistory(createBlock(xLength, yLength, history)); + } + }, [point, snake]); + + useEffect(() => { + if (isStarted) { + runSolution(); + } + }, [isStarted, point, snake]); + + useEffect(() => { + if (isRunning && values) { + refAnimationFrame.current = requestAnimationFrame(() => { + moveSnake(); + }); + } + }, [isRunning, values]); + + return ( + setLogs([]), + onPause: () => setIsRunning(false), + onPlay: () => setIsRunning(true), + onRefresh: () => runSolution(), + onReset: () => handleReset(), + onStart: () => setIsStarted(true), + onStepBackwards: () => setHistory(moveBackwards(history)), + onStepForwards: () => moveSnake(), + point: point, + snake: snake, + values: values, + xLength: xLength, + yLength: yLength, + }} + /> + ); +}; + +export default SnakeProvider; diff --git a/src/components/Projects/Snake/SnakeRunnerWorker.ts b/src/components/Projects/Snake/SnakeRunnerWorker.ts new file mode 100644 index 00000000..1b77def8 --- /dev/null +++ b/src/components/Projects/Snake/SnakeRunnerWorker.ts @@ -0,0 +1,37 @@ +onmessage = ({ data }) => { + const { fn, env } = data; + const { xMax, yMax, snake, point } = env; + const values: (string | number | undefined)[][] = []; + const snakeMap: Record = {}; + + for (let i = 0; i < snake.length - 1; i++) { + snakeMap[snake[i]] = true; + } + + try { + const heuristicFn = eval(`(function() {${fn}; return heuristic; })();`); + + for (let y = 0; y < yMax; y++) { + values[y] = []; + + for (let x = 0; x < xMax; x++) { + if (!snakeMap[[x, y].toString()]) { + const value = values[y][x] = heuristicFn([x, y], xMax, yMax, snake, point); + + if (typeof value === 'string' && isNaN(parseInt(value))) { + values[y][x] = NaN; + } + } else { + values[y][x] = undefined; + } + } + } + + } catch (error) { + /* eslint-disable no-console */ + console.error(error); + /* eslint-enable no-console */ + } + + postMessage({ values }); +}; diff --git a/src/components/Projects/Snake/SnakeScoreTile.tsx b/src/components/Projects/Snake/SnakeScoreTile.tsx new file mode 100644 index 00000000..51f8d17a --- /dev/null +++ b/src/components/Projects/Snake/SnakeScoreTile.tsx @@ -0,0 +1,32 @@ +import { Box, Text } from 'preshape'; +import React from 'react'; + +interface Props { + label: string; + value: number; +} + +const SnakeScoreTile = (props: Props) => { + const { value, label } = props; + + return ( + + + + {value} + {' '} + + {label} + + + + ); +}; + +export default SnakeScoreTile; diff --git a/src/components/Projects/Snake/SnakeScoreTiles.tsx b/src/components/Projects/Snake/SnakeScoreTiles.tsx new file mode 100644 index 00000000..9d6e0471 --- /dev/null +++ b/src/components/Projects/Snake/SnakeScoreTiles.tsx @@ -0,0 +1,24 @@ +import { Box } from 'preshape'; +import React from 'react'; +import { useSnakeContext } from './SnakeProvider'; +import SnakeScoreTile from './SnakeScoreTile'; +import getAverage from './utils/getAverage'; +import getPoints from './utils/getPoints'; +import getScore from './utils/getScore'; + +const SnakeScoreTiles = () => { + const { history, xLength, yLength } = useSnakeContext(); + + return ( + + + + + + ); +}; + +export default SnakeScoreTiles; diff --git a/src/components/Projects/Snake/SnakeViewer.tsx b/src/components/Projects/Snake/SnakeViewer.tsx new file mode 100644 index 00000000..e73ec3fc --- /dev/null +++ b/src/components/Projects/Snake/SnakeViewer.tsx @@ -0,0 +1,141 @@ +import { + colorNegativeShade4, + colorPositiveShade4, + colorWhite, + themes, + useResizeObserver, + Box, + BoxProps, + TypeTheme, +} from 'preshape'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import { useLayoutContext } from '../../Root'; +import { useSnakeContext } from './SnakeProvider'; +import getGradientColor from './utils/getGradientColor'; + +const padding = 0; + +type Props = BoxProps & { + theme?: TypeTheme; +}; + +const SnakeViewer = ({ theme: themeProps, ...rest }: Props) => { + const { point, snake, values, xLength, yLength } = useSnakeContext(); + const { theme: themeContext } = useLayoutContext(); + const theme = themeProps || themeContext; + + const [{ height, width }, refContainer] = useResizeObserver(); + const refCanvas = useRef(null); + + const cellStep = Math.floor( + Math.min((height - padding * 2) / yLength, (width - padding * 2) / xLength) + ); + const cellPadding = cellStep * 0.1; + const cellSize = cellStep - cellPadding; + const offsetX = padding + (width - cellStep * xLength) / 2; + const offsetY = padding + (height - cellStep * yLength) / 2; + + const redraw = () => { + if (refCanvas.current) { + const ctx = refCanvas.current.getContext('2d'); + + if (ctx) { + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = themes[theme].colorBackgroundShade1; + ctx.fillRect( + offsetX - padding, + offsetY - padding, + cellStep * xLength + padding, + cellStep * yLength + padding + ); + + for (let y = 0; y < yLength; y++) { + for (let x = 0; x < xLength; x++) { + const value = values && values[y] && values[y][x]; + const color = + value !== undefined && isNaN(value) + ? colorNegativeShade4 + : themes[theme].colorBackgroundShade2; + + ctx.fillStyle = color; + ctx.fillRect( + offsetX + x * cellStep, + offsetY + y * cellStep, + cellSize, + cellSize + ); + } + } + + if (point) { + ctx.fillStyle = colorPositiveShade4; + ctx.fillRect( + offsetX + point[0] * cellStep, + offsetY + point[1] * cellStep, + cellSize, + cellSize + ); + } + + for (let y = 0; y < yLength; y++) { + for (let x = 0; x < xLength; x++) { + const value = values && values[y] && values[y][x]; + + if (value !== undefined) { + ctx.fillStyle = + point && x === point[0] && y === point[1] + ? colorWhite + : themes[theme].colorTextShade1; + + ctx.textAlign = 'center'; + ctx.font = '"Courier New", Courier, monospace'; + ctx.fillText( + isNaN(value) ? 'NaN' : (+value.toFixed(2)).toString(), + Math.floor(offsetX + x * cellStep + cellSize / 2), + Math.floor(offsetY + y * cellStep + cellSize / 2) + 5 + ); + } + } + } + + if (snake) { + for (let i = 0; i < snake.length; i++) { + ctx.fillStyle = getGradientColor( + themeContext, + (snake.length - 1 - i) / snake.length + ); + ctx.fillRect( + offsetX + snake[i][0] * cellStep, + offsetY + snake[i][1] * cellStep, + cellSize, + cellSize + ); + } + } + } + } + }; + + useEffect(redraw, [point, snake, theme, themeContext, values]); + + useLayoutEffect(() => { + if (refCanvas.current) { + refCanvas.current.width = width * window.devicePixelRatio; + refCanvas.current.height = height * window.devicePixelRatio; + refCanvas.current.style.width = `${width}px`; + refCanvas.current.style.height = `${height}px`; + refCanvas.current + .getContext('2d') + ?.scale(window.devicePixelRatio, window.devicePixelRatio); + redraw(); + } + }, [refCanvas.current, height, width]); + + return ( + + + + ); +}; + +export default SnakeViewer; diff --git a/src/components/Projects/Snake/SolutionRunner.ts b/src/components/Projects/Snake/SolutionRunner.ts new file mode 100644 index 00000000..64eae184 --- /dev/null +++ b/src/components/Projects/Snake/SolutionRunner.ts @@ -0,0 +1,78 @@ +import { TypePoint, TypeSnake, TypeValues } from './types'; + +interface Config { + timeout: number; + worker: Worker; +} + +interface MessageData { + values: TypeValues; +} + +interface RunData { + env: { + point: TypePoint; + snake: TypeSnake; + xMax: number; + yMax: number; + }; + fn: string; +} + +type ErrorHandler = (error: Error | ErrorEvent) => void; +type MessageHandler = (data: MessageData) => void; + +export default class SolutionRunner { + config: Config; + onError?: ErrorHandler; + onMessage?: MessageHandler; + timeout?: NodeJS.Timeout; + worker: Worker; + + constructor(config: Config) { + this.config = config; + this.worker = config.worker; + + this.worker.onerror = (error) => { + this.reset(); + + if (this.onError) { + this.onError(error); + } + }; + + this.worker.onmessage = ({ data }: { data: MessageData }) => { + this.reset(); + + if (this.onMessage) { + this.onMessage(data); + } + }; + } + + run(args: RunData) { + if (!this.timeout) { + this.worker.postMessage(args); + this.timeout = setTimeout(() => { + const error = new Error('Timeout'); + error.message = `⏰ Your code exceeded the maximum ${this.config.timeout} ms run time.`; + + if (this.onError) { + this.onError(error); + } + }, this.config.timeout); + } + } + + reset() { + if (this.timeout) { + clearTimeout(this.timeout); + delete this.timeout; + } + } + + destroy() { + this.reset(); + this.worker.terminate(); + } +} diff --git a/src/components/Projects/Snake/solutions/blank.ts b/src/components/Projects/Snake/solutions/blank.ts new file mode 100644 index 00000000..551704f9 --- /dev/null +++ b/src/components/Projects/Snake/solutions/blank.ts @@ -0,0 +1,20 @@ +export default { + name: '📝 New Solution', + average: 0, + points: 0, + score: 0, + content: `/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snake Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + // You need to return a number here +}`, +}; diff --git a/src/components/Projects/Snake/solutions/euclideanDistance.ts b/src/components/Projects/Snake/solutions/euclideanDistance.ts new file mode 100644 index 00000000..3b1263df --- /dev/null +++ b/src/components/Projects/Snake/solutions/euclideanDistance.ts @@ -0,0 +1,21 @@ +export default { + name: '📐Euclidean Distance', + average: 10, + points: 35, + score: 1031, + content: ` +/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snake Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + return Math.hypot(cell[0] - point[0], cell[1] - point[1]); +}`, +}; diff --git a/src/components/Projects/Snake/solutions/hamiltonianCycle.ts b/src/components/Projects/Snake/solutions/hamiltonianCycle.ts new file mode 100644 index 00000000..1d5da80b --- /dev/null +++ b/src/components/Projects/Snake/solutions/hamiltonianCycle.ts @@ -0,0 +1,72 @@ +export default { + name: '🌀Hamiltonian Cycle', + average: 76, + points: 195, + score: 4303, + content: `const DIR_U = 0; +const DIR_R = 1; +const DIR_D = 2; +const DIR_L = 3; + +const getDirection = (a, b) => + (a[1] === b[1] && a[0] === b[0] - 1 && DIR_R) || + (a[0] === b[0] && a[1] === b[1] - 1 && DIR_D) || + (a[1] === b[1] && a[0] === b[0] + 1 && DIR_L) || + (a[0] === b[0] && a[1] === b[1] + 1 && DIR_U); + +/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snake Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + const xMax = xLength - 1; + const yMax = yLength - 1; + const [headX, headY] = snake[0]; + const dirCell = getDirection(snake[0], cell); + const dirCurrent = getDirection(snake[1], snake[0]); + + if (dirCurrent === DIR_R) { + // Continuing sweep movement + if ((headX < xMax - 1 || headY === yMax) && dirCell === dirCurrent) return 0; + + // Moving onto the next row when approaching the edge + if (headX >= xMax - 1 && dirCell === DIR_D) return 0; + } + + if (dirCurrent === DIR_L) { + // Continuing sweep movement + if ((headX > 1 || headY === yMax) && dirCell === dirCurrent) return 0; + + // Moving onto the next row when approaching the edge + if (headX <= 1 && dirCell === DIR_D) return 0; + } + + if (dirCurrent === DIR_R || dirCurrent === DIR_L) { + // When at the bounds on the last line, head back up + if ((headX === 0 || headX === xMax) && headY === yMax && dirCell === DIR_U) return 0; + } + + if (dirCurrent === DIR_D) { + // After moving onto the next line, get the next direction + if ((headX < xLength / 2 && dirCell === DIR_R)) return 0; + if ((headX > xLength / 2 && dirCell === DIR_L)) return 0; + } + + if (dirCurrent === DIR_U) { + // Continue moving back up to the top of the border + if (dirCell === DIR_U) return 0; + + // After returning to the top, set the direction to continue sweeping + if ((headY === 0 || headY === yMax) && headX === 0 && (dirCell === DIR_L || dirCell === DIR_R)) return 0; + } + + return 999; +}`, +}; diff --git a/src/components/Projects/Snake/solutions/index.ts b/src/components/Projects/Snake/solutions/index.ts new file mode 100644 index 00000000..bda663b4 --- /dev/null +++ b/src/components/Projects/Snake/solutions/index.ts @@ -0,0 +1,26 @@ +import { ISolutionWithScore } from '../Types'; +import hamiltonianCycle from './hamiltonianCycle'; +import blank from './blank'; +import euclideanDistance from './euclideanDistance'; +import manhattanDistance from './manhattanDistance'; +import random from './random'; +import tailEscape from './tailEscape'; + +export { + hamiltonianCycle, + blank, + euclideanDistance, + manhattanDistance, + random, + tailEscape, +}; + +const solutions: ISolutionWithScore[] = [ + hamiltonianCycle, + euclideanDistance, + manhattanDistance, + random, + tailEscape, +].sort((a, b) => b.score - a.score); + +export default solutions; diff --git a/src/components/Projects/Snake/solutions/manhattanDistance.ts b/src/components/Projects/Snake/solutions/manhattanDistance.ts new file mode 100644 index 00000000..59d5958d --- /dev/null +++ b/src/components/Projects/Snake/solutions/manhattanDistance.ts @@ -0,0 +1,20 @@ +export default { + name: '🗽Manhattan Distance', + average: 11, + points: 35, + score: 1012, + content: `/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snake Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + return Math.abs(cell[0] - point[0]) + Math.abs(cell[1] - point[1]); +}`, +}; diff --git a/src/components/Projects/Snake/solutions/random.ts b/src/components/Projects/Snake/solutions/random.ts new file mode 100644 index 00000000..ae0a69c9 --- /dev/null +++ b/src/components/Projects/Snake/solutions/random.ts @@ -0,0 +1,20 @@ +export default { + name: '❓Math.random', + average: 110, + points: 4, + score: 6, + content: `/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snake Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + return Math.random(); +}`, +}; diff --git a/src/components/Projects/Snake/solutions/tailEscape.ts b/src/components/Projects/Snake/solutions/tailEscape.ts new file mode 100644 index 00000000..a38db1eb --- /dev/null +++ b/src/components/Projects/Snake/solutions/tailEscape.ts @@ -0,0 +1,72 @@ +export default { + name: '🐕Tail escape', + average: 29, + points: 220, + score: 22685, + content: `const adjacentes = (a, xMax, yMax) => [[a[0], a[1] - 1], [a[0] + 1, a[1]], [a[0], a[1] + 1], [a[0] - 1, a[1]]].filter(b => b[0] >= 0 && b[1] >= 0 && b[0] <= xMax && b[1] <= yMax); +const equals = ([x1, y1], [x2, y2]) => x1 === x2 && y1 === y2; +const includes = (a, b) => a.some((a) => equals(a, b)); +const difference = (a, b) => a.filter((a) => !includes(b, a)); +const shift = (a, b, collect) => b.concat(a).slice(0, b.length + (a.length - b.length + (collect ? 1 : 0))); +const tail = (a) => a[a.length - 1]; + +const search = (start, end, xMax, yMax, snake) => { + const queue = [start]; + const paths = { [start]: [start] }; + + while (queue.length) { + const current = queue.shift(); + const snakeShifted = shift(snake, paths[current] = paths[current] || [start]); + + if (equals(current, end)) { + return paths[current]; + } + + for (const next of difference(adjacentes(current, xMax, yMax), snakeShifted)) { + if (!(next in paths)) { + queue.push(next); + paths[next] = [next].concat(paths[current]); + } + } + } +}; + +/** + * The heuristic function will be run on every cell, and should return a number. The number that is returned will be used to determine the path of the snake. + * + * @param [number, number] cell Coordinates of the cell to return a value for + * @param number xLength The number of cells across the x axis + * @param number yLength The number of cells across the y axis + * @param [number, number][] snakeOrigin Coordinates of the position of the snake from head to tail. E.g. [[4, 1], [3, 1]] + * @param [number, number] point Coordinates of the point. + * + * @returns number The value for the cell + */ +function heuristic(cell, xLength, yLength, snake, point) { + const size = (xLength * yLength) * 2; + const xMax = xLength - 1; + const yMax = yLength - 1; + + if (!includes(adjacentes(snake[0], xMax, yMax), cell)) return 0; + + const pathToPoint = search(cell, point, xMax, yMax, snake); + + if (pathToPoint) { + const snakeAtPoint = shift(snake, pathToPoint, true); + + for (const next of difference(adjacentes(point, xMax, yMax), snakeAtPoint)) { + if (search(next, tail(snakeAtPoint), xMax, yMax, snakeAtPoint)) { + return pathToPoint.length; + } + } + } + + const pathToTail = search(cell, tail(snake), xMax, yMax, snake); + + if (pathToTail) { + return size - pathToTail.length; + } + + return size * 2; +}`, +}; diff --git a/src/components/Projects/Snake/types.ts b/src/components/Projects/Snake/types.ts new file mode 100644 index 00000000..b4532f6b --- /dev/null +++ b/src/components/Projects/Snake/types.ts @@ -0,0 +1,25 @@ +export type TypeCell = [number, number]; + +export type TypePath = TypeCell[]; +export type TypePoint = TypeCell; +export type TypeSnake = TypeCell[]; +export type TypeValues = number[][]; + +export interface Environment { + path: TypePath; + point: undefined | TypePoint; + snake: TypeSnake; +} + +export type TypeHistory = Environment[]; + +export interface TypeSolution { + name: string; + content: string; +} + +export interface TypeSolutionWithScore extends TypeSolution { + average: number; + points: number; + score: number; +} diff --git a/src/components/Projects/Snake/utils/environment.ts b/src/components/Projects/Snake/utils/environment.ts new file mode 100644 index 00000000..aef90154 --- /dev/null +++ b/src/components/Projects/Snake/utils/environment.ts @@ -0,0 +1,27 @@ +import { IEnvironment, TypeCell, TypeSnake } from '../types'; +import isCellIncluded from './isCellIncluded'; + +export const createSnake = (xLength: number, yLength: number): TypeSnake => + Array.from({ length: 4 }, (_, i) => [ + Math.floor(xLength / 2) - i, + Math.floor(yLength / 2), + ]); + +export const createPoint = (xLength: number, yLength: number, snake: TypeSnake): undefined | TypeCell => { + const freeGrid = Array + .from({ length: xLength * yLength }) + .map<[number, number]>((_, i) => [i % xLength, Math.floor(i / yLength)]) + .filter((cell) => !isCellIncluded(snake, cell)); + + return freeGrid.length + ? freeGrid[Math.floor(Math.random() * (freeGrid.length - 1))] + : undefined; +}; + +export const createEnvironment = (xLength: number, yLength: number, snake: TypeSnake = createSnake(xLength, yLength)): IEnvironment => { + return { + path: [], + point: createPoint(xLength, yLength, snake), + snake: snake, + }; +}; diff --git a/src/components/Projects/Snake/utils/getAverage.ts b/src/components/Projects/Snake/utils/getAverage.ts new file mode 100644 index 00000000..3bc2bb75 --- /dev/null +++ b/src/components/Projects/Snake/utils/getAverage.ts @@ -0,0 +1,6 @@ +import { TypeHistory } from '../types'; +import getCompletedHistory from './getCompletedHistory'; +import getMean from './getMean'; + +export default (history: TypeHistory) => + getMean(getCompletedHistory(history).map(({ path }) => path.length)); diff --git a/src/components/Projects/Snake/utils/getCompletedHistory.ts b/src/components/Projects/Snake/utils/getCompletedHistory.ts new file mode 100644 index 00000000..5bdba57c --- /dev/null +++ b/src/components/Projects/Snake/utils/getCompletedHistory.ts @@ -0,0 +1,12 @@ +import { TypeHistory } from '../types'; +import isCellEqual from './isCellEqual'; + +export default (history: TypeHistory) => { + const final = history[history.length - 1]; + + if (final && final.point && isCellEqual(final.snake[0], final.point)) { + return history; + } + + return history.slice(0, -1); +}; diff --git a/src/components/Projects/Snake/utils/getGradientColor.ts b/src/components/Projects/Snake/utils/getGradientColor.ts new file mode 100644 index 00000000..6902f90c --- /dev/null +++ b/src/components/Projects/Snake/utils/getGradientColor.ts @@ -0,0 +1,33 @@ +import { themes, TypeTheme } from 'preshape'; + +const hexToRgb = (hex: string, int = parseInt(hex.replace('#', ''), 16)): [number, number, number] => + [(int >> 16) & 255, (int >> 8) & 255, int & 255]; + +const themeGradients = (Object.keys(themes) as TypeTheme[]) + .reduce<{ [key in TypeTheme]?: [number, [number, number, number]][] }>((theme, key) => ({ + ...theme, + [key]: [ + [0, hexToRgb(themes[key].colorAccentShade4)], + [1, hexToRgb(themes[key].colorTextShade2)], + ], + }), {}); + + +export default (theme: TypeTheme, percent: number) => { + const GRADIENT = themeGradients[theme]; + + if (GRADIENT) { + const [lp, [lr, lg, lb]] = GRADIENT[0]; + const [up, [ur, ug, ub]] = GRADIENT[1]; + const upperPercentage = (percent - lp) / (up - lp); + const lowerPercentage = 1 - upperPercentage; + + return `rgb( + ${Math.floor(lr * lowerPercentage + ur * upperPercentage)}, + ${Math.floor(lg * lowerPercentage + ug * upperPercentage)}, + ${Math.floor(lb * lowerPercentage + ub * upperPercentage)} + )`; + } + + return ''; +}; diff --git a/src/components/Projects/Snake/utils/getMean.ts b/src/components/Projects/Snake/utils/getMean.ts new file mode 100644 index 00000000..c269969b --- /dev/null +++ b/src/components/Projects/Snake/utils/getMean.ts @@ -0,0 +1,3 @@ +export default (ns: number[]) => ns.length + ? ns.reduce((acc, n) => acc + n, 0) / ns.length + : 0; diff --git a/src/components/Projects/Snake/utils/getPoints.ts b/src/components/Projects/Snake/utils/getPoints.ts new file mode 100644 index 00000000..ff2afe03 --- /dev/null +++ b/src/components/Projects/Snake/utils/getPoints.ts @@ -0,0 +1,6 @@ +import { TypeHistory } from '../types'; +import getCompletedHistory from './getCompletedHistory'; + +export default (history: TypeHistory) => { + return getCompletedHistory(history).length; +}; diff --git a/src/components/Projects/Snake/utils/getScore.ts b/src/components/Projects/Snake/utils/getScore.ts new file mode 100644 index 00000000..35370db7 --- /dev/null +++ b/src/components/Projects/Snake/utils/getScore.ts @@ -0,0 +1,21 @@ +import { TypeHistory } from '../types'; +import getCompletedHistory from './getCompletedHistory'; +import getMean from './getMean'; + +export default (xLength: number, yLength: number, history: TypeHistory) => { + const nCells = xLength * yLength; + const moves = getCompletedHistory(history).map(({ path }) => path.length); + + let score = 0; + + for (let point = 1; point < moves.length + 1; point++) { + const a = getMean(moves.slice(0, point)); + const wA = (1 - a / nCells) + 1; + const wM = (1 - moves[point - 1] / nCells) + 1; + const wP = 1 - point / nCells; + + score += Math.max(point, point * wP * (wA * wM)); + } + + return score; +}; diff --git a/src/components/Projects/Snake/utils/getSurroundingCells.ts b/src/components/Projects/Snake/utils/getSurroundingCells.ts new file mode 100644 index 00000000..8c92ff55 --- /dev/null +++ b/src/components/Projects/Snake/utils/getSurroundingCells.ts @@ -0,0 +1,15 @@ +import { TypeCell, TypeSnake } from '../types'; +import isCellInbounds from './isCellInbounds'; +import isCellIncluded from './isCellIncluded'; + +export default (xLength: number, yLength: number, snake: TypeSnake) => { + const cells: TypeCell[] = [ + [snake[0][0], snake[0][1] - 1], + [snake[0][0] + 1, snake[0][1]], + [snake[0][0], snake[0][1] + 1], + [snake[0][0] - 1, snake[0][1]], + ]; + + return cells.filter((cell) => isCellInbounds(xLength, yLength, cell) && + !isCellIncluded(snake.slice(0, -1), cell)); +}; diff --git a/src/components/Projects/Snake/utils/history.ts b/src/components/Projects/Snake/utils/history.ts new file mode 100644 index 00000000..dfcd255e --- /dev/null +++ b/src/components/Projects/Snake/utils/history.ts @@ -0,0 +1,30 @@ +import { IEnvironment, TypeCell, TypeHistory } from '../types'; +import { createEnvironment } from './environment'; + + +export const manipulateHistory = (history: TypeHistory, n: number, predicate: (block: IEnvironment) => null | IEnvironment): TypeHistory => { + const entry = predicate(history[n]); + + return entry + ? [...history.slice(0, n), entry, ...history.slice(n + 1)] + : [...history.slice(0, n), ...history.slice(n + 1)]; +}; + +export const createBlock = (xSize: number, ySize: number, history: TypeHistory) => + manipulateHistory(history, history.length, () => + createEnvironment(xSize, ySize, history[history.length - 1] && history[history.length - 1].snake)); + +export const moveForwards = (history: TypeHistory, cell: TypeCell, extend?: boolean) => + manipulateHistory(history, history.length - 1, ({ path, point, snake }) => ({ + path: [snake.slice(-1)[0], ...path], + point: point, + snake: [cell, ...snake].slice(0, extend ? undefined : -1), + })); + +export const moveBackwards = (history: TypeHistory) => + manipulateHistory(history, history.length - 1, ({ path, point, snake }) => + path.length === 0 ? null : ({ + path: path.slice(1), + point: point, + snake: [...snake.slice(1), path[0]], + })); diff --git a/src/components/Projects/Snake/utils/isCellEqual.ts b/src/components/Projects/Snake/utils/isCellEqual.ts new file mode 100644 index 00000000..3cf8ad3f --- /dev/null +++ b/src/components/Projects/Snake/utils/isCellEqual.ts @@ -0,0 +1,5 @@ +import { TypeCell } from '../types'; + +export default (a: TypeCell, b: TypeCell) => { + return a[0] === b[0] && a[1] === b[1]; +}; diff --git a/src/components/Projects/Snake/utils/isCellInbounds.ts b/src/components/Projects/Snake/utils/isCellInbounds.ts new file mode 100644 index 00000000..35312f87 --- /dev/null +++ b/src/components/Projects/Snake/utils/isCellInbounds.ts @@ -0,0 +1,5 @@ +import { TypeCell } from '../types'; + +export default (xLength: number, yLength: number, [x, y]: TypeCell) => + x >= 0 && x < xLength && + y >= 0 && y < yLength; diff --git a/src/components/Projects/Snake/utils/isCellIncluded.ts b/src/components/Projects/Snake/utils/isCellIncluded.ts new file mode 100644 index 00000000..9d5475a6 --- /dev/null +++ b/src/components/Projects/Snake/utils/isCellIncluded.ts @@ -0,0 +1,5 @@ +import { TypeCell } from '../types'; +import isCellEqual from './isCellEqual'; + +export default (set: TypeCell[], a: TypeCell) => + set.some((b) => isCellEqual(a, b)); diff --git a/src/components/Publication/Publication.tsx b/src/components/Publication/Publication.tsx new file mode 100644 index 00000000..661531fb --- /dev/null +++ b/src/components/Publication/Publication.tsx @@ -0,0 +1,52 @@ +import { Link, Text } from 'preshape'; +import React from 'react'; +import { Publication } from '../../types'; +import { fromISO } from '../../utils/date'; + +interface Props extends Publication {} + +const PublicationComponent = (props: Props) => { + const { authors, date, description, journal, title, href } = props; + + return ( + + + + {title} + + + + {journal} + + + + {description} + + + by{' '} + {authors.map((author, index) => ( + + {author}{' '} + + [{index + 1}] + + {index === authors.length - 1 ? ' ' : ', '} + + ))} + + + + {fromISO(date)} + + + ); +}; + +export default PublicationComponent; diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 7a7cf888..04f6793a 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -5,16 +5,17 @@ import { Box, useMatchMedia, } from 'preshape'; -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { Route, Routes, useLocation } from 'react-router-dom'; import data from '../data'; import Landing from './Landing/Landing'; import Metas from './Metas/Metas'; +import CircleArt from './Projects/CircleArt/CircleArt'; import CircleGraph from './Projects/CircleGraph/CircleGraph'; +import Snake from './Projects/Snake/Snake'; import Spirals from './Projects/Spirals/Spirals'; import CircleGraphs from './Writings/CircleGraphs/CircleGraphs'; import CircleIntersections from './Writings/CircleIntersections/CircleIntersections'; -import GeneratingTessellations from './Writings/GeneratingTessellations/GeneratingTessellations'; import SnakeSolution from './Writings/SnakeSolution/SnakeSolution'; export const RootContext = createContext<{ @@ -27,6 +28,8 @@ export const RootContext = createContext<{ theme: 'day', }); +export const useLayoutContext = () => useContext(RootContext); + const Site = () => { const [theme, onChangeTheme] = useState('day'); const location = useLocation(); @@ -52,11 +55,13 @@ const Site = () => { } /> + } path={data.projects.CircleArt.to} /> } path={data.projects.CircleGraph.to} /> } path={data.projects.Spirals.to} /> + } path={data.projects.Snake.to} /> } path={data.writings.CircleIntersections.to} @@ -65,10 +70,10 @@ const Site = () => { element={} path={data.writings.CircleGraphs.to} /> - } path={data.writings.GeneratingTessellations.to} - /> + /> */} } path={data.writings.SnakeSolution.to} diff --git a/src/components/Writings/CircleGraphs/CircleGraphs.tsx b/src/components/Writings/CircleGraphs/CircleGraphs.tsx index 6b956d92..7e0193eb 100644 --- a/src/components/Writings/CircleGraphs/CircleGraphs.tsx +++ b/src/components/Writings/CircleGraphs/CircleGraphs.tsx @@ -55,7 +55,7 @@ const CircleGraphs = () => { const match = useMatchMedia(['600px']); const refVisualisation = useRef(null); - const resultUseGraphHook = useGraph(sampleCircles, traversals); + const resultUseGraphHook = useGraph(sampleCircles, { traversals }); const handleSetTraversals = ( activeNodeIndex: number, diff --git a/src/components/Writings/SnakeSolution/SnakeRunner.tsx b/src/components/Writings/SnakeSolution/SnakeRunner.tsx index 5f4a1255..b7a72f23 100644 --- a/src/components/Writings/SnakeSolution/SnakeRunner.tsx +++ b/src/components/Writings/SnakeSolution/SnakeRunner.tsx @@ -1,5 +1,5 @@ -import { Snake } from '@hhogg/snake'; import React, { useRef } from 'react'; +import SnakeProvider from '../../Projects/Snake/SnakeProvider'; import SnakeRunnerViewer from './SnakeRunnerViewer'; interface Props { @@ -9,13 +9,13 @@ interface Props { const SnakeRunner = (props: Props) => { const { solution } = props; const worker = useRef( - new Worker('../../../../node_modules/@hhogg/snake/src/SnakeRunnerWorker.js') + new Worker('../../Projects/Snake/SnakeRunnerWorker.ts') ); return ( - + - + ); }; diff --git a/src/components/Writings/SnakeSolution/SnakeRunnerViewer.tsx b/src/components/Writings/SnakeSolution/SnakeRunnerViewer.tsx index 8a4456f2..9298a3d2 100644 --- a/src/components/Writings/SnakeSolution/SnakeRunnerViewer.tsx +++ b/src/components/Writings/SnakeSolution/SnakeRunnerViewer.tsx @@ -1,4 +1,3 @@ -import { getAverage, getScore, SnakeContext, SnakeViewer } from '@hhogg/snake'; import { Appear, Buttons, @@ -12,8 +11,12 @@ import { Text, useIntersectionObserver, } from 'preshape'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import 'brace/mode/javascript'; +import { useSnakeContext } from '../../Projects/Snake/SnakeProvider'; +import SnakeViewer from '../../Projects/Snake/SnakeViewer'; +import getAverage from '../../Projects/Snake/utils/getAverage'; +import getScore from '../../Projects/Snake/utils/getScore'; interface Props { solution: string; @@ -22,7 +25,7 @@ interface Props { const SnakeRunnerViewer = (props: Props) => { const { solution } = props; const { history, onPause, onPlay, onStart, onReset, xLength, yLength } = - useContext(SnakeContext); + useSnakeContext(); const [isCodeVisible, setCodeVisible] = useState(false); const [isInView, ref] = useIntersectionObserver(); diff --git a/src/components/Writings/SnakeSolution/SnakeSolutionComparison.tsx b/src/components/Writings/SnakeSolution/SnakeSolutionComparison.tsx index 33148957..72aaf0bb 100644 --- a/src/components/Writings/SnakeSolution/SnakeSolutionComparison.tsx +++ b/src/components/Writings/SnakeSolution/SnakeSolutionComparison.tsx @@ -104,6 +104,7 @@ const SnakeSolutionComparison = (props: Props) => { = { +const data: Data = { experience: { Pure360: { company: 'Pure360', @@ -45,6 +38,7 @@ const data: Data< company: 'Spotify', date: '2021-08-30', role: 'Senior Engineer', + tags: ['typescript', 'nodejs', 'java', 'react', 'gcp'], }, }, @@ -58,13 +52,13 @@ const data: Data< title: 'Circle Graph', to: '/projects/circle-graph', }, - Circles: { + CircleArt: { description: 'A web application for creating artwork by filling in the intersection areas of overlapping circles. Using an experimental way of calculating intersections areas with graphs.', - href: 'https://circles.hogg.io', image: require('./assets/circles.svg'), tags: ['typescript', 'react', 'geometry'], - title: 'Circles', + title: 'Circle Art', + to: '/projects/circle-art' }, Antwerp: { description: @@ -84,8 +78,8 @@ const data: Data< }, Snake: { description: - 'Snake Heuristics is a game for developers to write a heuristic function, to play the perfect classic game of snake. This was created for a workshop at the AsyncJS meetup.', - href: 'https://snake.hogg.io', + 'A project that was originally created as a game for developers to compete on writing a heuristic function to complete the game of snake in the most efficient way possible.', + to: '/projects/snakes', image: require('./assets/snake.svg'), tags: ['javascript', 'react', 'css'], title: 'Snake', @@ -138,23 +132,23 @@ const data: Data< to: '/writings/circle-intersections', unlisted: true, }, - GeneratingTessellations: { - id: 'GeneratingTessellations', - date: '2020-01-31', - description: - 'An explanation of the GomJau-Hogg’s notation for generating all of the regular, semiregular (uniform) and demigular (k-uniform, up to at least k=3) in a consistent, unique and unequivocal manner.', - imageOG: require('./assets/antwerp.png'), - tags: [ - 'svg', - 'visualisation', - 'geometry', - 'tessellations', - 'nomenclature', - ], - title: - 'GomJau-Hogg’s notation for automatic generation of k-uniform tessellations', - to: '/writings/generating-tessellations', - }, + // GeneratingTessellations: { + // id: 'GeneratingTessellations', + // date: '2020-01-31', + // description: + // 'An explanation of the GomJau-Hogg’s notation for generating all of the regular, semiregular (uniform) and demigular (k-uniform, up to at least k=3) in a consistent, unique and unequivocal manner.', + // imageOG: require('./assets/antwerp.png'), + // tags: [ + // 'svg', + // 'visualisation', + // 'geometry', + // 'tessellations', + // 'nomenclature', + // ], + // title: + // 'GomJau-Hogg’s notation for automatic generation of k-uniform tessellations', + // to: '/writings/generating-tessellations', + // }, SnakeSolution: { id: 'SnakeSolution', date: '2020-04-13', @@ -174,6 +168,17 @@ const data: Data< to: '/writings/snake-solution', }, }, + + publications: { + Tilings: { + title: 'GomJau-Hogg’s Notation for Automatic Generation of k-Uniform Tessellations with ANTWERP v3.0', + date: '2021-12-09', + authors: ['Valentin Gomez-Jauregui', 'Harrison Hogg', 'Cristina Manchado', 'Cesar Otero'], + journal: 'MDPI Symmetry', + description: 'Euclidean tilings are constantly applied to many fields of engineering (mechanical, civil, chemical, etc.). These tessellations are usually named after Cundy & Rollett’s notation. However, this notation has two main problems related to ambiguous conformation and uniqueness. This communication explains the GomJau-Hogg’s notation for generating all the regular, semi-regular (uniform) and demi-regular (k-uniform, up to at least k = 3) in a consistent, unique and unequivocal manner. Moreover, it presents Antwerp v3.0, a free online application, which is publicly shared to prove that all the basic tilings can be obtained directly from the GomJau-Hogg’s notation.', + href: 'https://www.mdpi.com/2073-8994/13/12/2376', + } + }, }; export const listedWritingsSorted = Object.values(data.writings) @@ -184,4 +189,8 @@ export const experienceSorted = Object.values(data.experience).sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); +export const publicationsSorted = Object.values(data.publications).sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() +); + export default data; diff --git a/src/types.ts b/src/types.ts index 052e7b5c..de2d8140 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,17 @@ -export interface Experience { + +type ExperienceList = +| 'Pure360' | 'Reedsy' | 'Brandwatch' | 'Bitrise' | 'Spotify'; + +type ProjectsList = +| 'Antwerp' | 'CircleGraph' | 'CircleArt' | 'Preshape' | 'Snake' | 'Spirals'; + +type WritingsList = +| 'CircleGraphs' | 'CircleIntersections' | 'SnakeSolution' + +type PublicationsList = +| 'Tilings'; + +export type Experience = { company: string; date: string; description?: string; @@ -6,7 +19,7 @@ export interface Experience { role: string; } -export interface Project { +export type Project = { description: string; href?: string; image: string; @@ -16,7 +29,7 @@ export interface Project { to?: string; } -export interface Writing { +export type Writing = { id: string; date: string; description: string; @@ -27,8 +40,18 @@ export interface Writing { unlisted?: boolean; } -export interface Data { - experience: Record; - projects: Record; - writings: Record; +export type Publication = { + title: string; + date: string; + journal: string; + authors: string[]; + href: string; + description: string; +}; + +export interface Data { + experience: Record; + projects: Record; + writings: Record; + publications: Record, } diff --git a/src/utils/Tween.js b/src/utils/Tween.js deleted file mode 100644 index 3fd1f19b..00000000 --- a/src/utils/Tween.js +++ /dev/null @@ -1,8 +0,0 @@ -export const onCompleteAll = (tweens) => - Promise.all( - tweens - .filter((_) => _) - .map( - (tween) => new Promise((resolve) => tween.onComplete(resolve).start()) - ) - ); diff --git a/src/utils/Two.js b/src/utils/Two.js deleted file mode 100644 index 8530d15a..00000000 --- a/src/utils/Two.js +++ /dev/null @@ -1,189 +0,0 @@ -import Two from 'two.js'; - -const PI = Math.PI; -const HALF_PI = PI / 2; - -const createShape = (shape, props) => { - const { - fill = 'transparent', - fillOpacity = 1, - opacity = 1, - rotate = 0, - stroke = 'transparent', - strokeWidth = 0, - translate, - } = props; - - const { top, left, width, height } = shape.getBoundingClientRect(); - - const cx = left + width / 2; - const cy = top + height / 2; - - if (translate) { - shape.center(); - shape.translation.set(cx, cy); - } - - shape.fill = fill; - shape.fillOpacity = fillOpacity; - shape.stroke = stroke; - shape.linewidth = strokeWidth; - shape.opacity = opacity; - shape.rotation = rotate; - - return shape; -}; - -export const addClassName = (shape, className) => { - shape.domElement = shape.domElement || document.getElementById(shape.id); - className.split(' ').forEach((className) => { - shape.domElement.classList.add(className); - }); - return shape; -}; - -export const setAttribute = (shape, attribute, value) => { - shape.domElement = shape.domElement || document.getElementById(shape.id); - shape.domElement.setAttribute(attribute, value); - return shape; -}; - -export const setClassName = (shape, className) => - setAttribute(shape, 'class', className); - -export const createArc = (props) => { - return createShape( - new Two.Path(arcsToAnchors([props]), false, false, true), - props - ); -}; - -export const createCircle = (props) => { - return createShape(new Two.Circle(props.x, props.y, props.radius), props); -}; - -export const createEllipse = (props) => { - return createShape( - new Two.Ellipsie(props.x, props.y, props.width, props.height), - props - ); -}; - -export const createGroup = (props = {}) => { - const group = new Two.Group(); - - if (props.x !== undefined && props.y !== undefined) { - group.translation.set(props.x, props.y); - } - - return group; -}; - -export const createLine = (props) => { - return createShape( - new Two.Path( - props.vertices.map(([x, y]) => new Two.Vector(x, y)), - false, - props.curved - ), - props - ); -}; - -export const createPolygon = (props) => { - return createShape( - new Two.Path( - props.vertices.map(([x, y]) => new Two.Vector(x, y)), - true, - props.curved - ), - props - ); -}; - -export const createPolygonArc = (props) => { - return createShape( - new Two.Path(arcsToAnchors(props.arcs, true), true, false, true), - props - ); -}; - -export const createText = (text, props) => { - return createShape(new Two.Text(text, props.x, props.y, props), props); -}; - -export const createTriangle = (props) => { - return createShape( - new Two.Path( - [ - new Two.Vector(props.x, props.y - props.height / 2), - new Two.Vector(props.x + props.width / 2, props.y + props.height / 2), - new Two.Vector(props.x - props.width / 2, props.y + props.height / 2), - ], - true - ), - props - ); -}; - -const arcsToAnchors = (arcs, closed) => { - const R = Two.Resolution * 3; - const anchors = Array.from({ length: R * arcs.length }).map( - () => new Two.Anchor() - ); - - for (let i = 0; i < arcs.length; i++) { - const { a1, a2, cx, cy, radius } = arcs[i]; - - for (let j = 0; j < R; j++) { - const anchorIndex = i * R + j; - const anchor = anchors[anchorIndex]; - const theta = (j / (R - 1)) * (a2 - a1) + a1; - - if (i === 0 && j === 0) { - anchor.command = Two.Commands.move; - } else { - anchor.command = Two.Commands.curve; - } - - anchor.x = cx + radius * Math.cos(theta); - anchor.y = cy + radius * Math.sin(theta); - - if (anchor.controls) { - anchor.controls.left.clear(); - anchor.controls.right.clear(); - } - - if (anchor.command === Two.Commands.curve) { - const amp = (radius * ((a2 - a1) / R)) / PI; - - if (j !== 0) { - anchor.controls.left.x = amp * Math.cos(theta - HALF_PI); - anchor.controls.left.y = amp * Math.sin(theta - HALF_PI); - } - - if (j !== R - 1) { - anchor.controls.right.x = amp * Math.cos(theta + HALF_PI); - anchor.controls.right.y = amp * Math.sin(theta + HALF_PI); - } - } - } - } - - if (closed) { - anchors[anchors.length - 1].x = anchors[0].x; - anchors[anchors.length - 1].y = anchors[0].y; - } - - return anchors; -}; - -export const onMouseDownGlobal = () => { - document.body.style.userSelect = 'none'; - document.body.style.webkitUserDrag = 'none'; -}; - -export const onMouseUpGlobal = () => { - document.body.style.userSelect = null; - document.body.style.webkitUserDrag = null; -}; diff --git a/src/utils/moveEvent.js b/src/utils/moveEvent.js deleted file mode 100644 index 69c77e1d..00000000 --- a/src/utils/moveEvent.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (event) => - event.touches - ? { - clientX: event.touches[0].clientX, - clientY: event.touches[0].clientY, - target: event.target, - } - : { - clientX: event.clientX, - clientY: event.clientY, - target: event.target, - }; diff --git a/src/utils/useEnforcedTheme.ts b/src/utils/useEnforcedTheme.ts new file mode 100644 index 00000000..e4c3d91c --- /dev/null +++ b/src/utils/useEnforcedTheme.ts @@ -0,0 +1,18 @@ +import { TypeTheme } from "preshape"; +import { useEffect, useRef } from "react"; +import { useLayoutContext } from "../components/Root"; + +const useEnforcedTheme = (theme: TypeTheme) => { + const { onChangeTheme, theme: currentTheme, } = useLayoutContext(); + const refPreviousTheme = useRef(currentTheme); + + useEffect(() => { + onChangeTheme(theme); + + return () => { + onChangeTheme(refPreviousTheme.current); + }; + }, []); +}; + +export default useEnforcedTheme; diff --git a/tsconfig.json b/tsconfig.json index 7837fa50..1acf5943 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "allowSyntheticDefaultImports": true, - "baseUrl": "./", "esModuleInterop": true, "jsx": "react", "module": "esnext", diff --git a/yarn.lock b/yarn.lock index 33d0345a..a8dc2f0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1641,6 +1641,11 @@ dependencies: "@types/node" "*" +"@types/file-saver@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" + integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ== + "@types/fs-extra@^8.0.1": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" @@ -1807,6 +1812,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== +"@types/uuid@*": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@typescript-eslint/eslint-plugin@^5.13.0": version "5.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.16.0.tgz#78f246dd8d1b528fc5bfca99a8a64d4023a3d86d" @@ -2462,10 +2472,10 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== -base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== base@^0.11.1: version "0.11.2" @@ -5488,6 +5498,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -10170,13 +10185,12 @@ pkginfo@0.3.x: integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= plist@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c" - integrity sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ== + version "3.0.6" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" + integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== dependencies: - base64-js "^1.2.3" - xmlbuilder "^9.0.7" - xmldom "0.1.x" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" pn@^1.1.0: version "1.1.0" @@ -10963,10 +10977,10 @@ prepend-http@^1.0.1: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -preshape@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/preshape/-/preshape-12.0.1.tgz#3cb5bbf5953b056c19867fefc54e869ee729593f" - integrity sha512-QWO4/AJhOBaA9ePFCh6T6lwGf9mggH+hfldU1yKBPazxJRLtpPVUikDH4CLKVsEMA6wGpQkaSU/TZCl1Df2y5A== +preshape@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/preshape/-/preshape-13.0.0.tgz#20f14478330f39efeccaa9eb3ace9a39261994cf" + integrity sha512-lW4dgXioRtlGDpsYD0bJNEI5z2GRZ0knI0Z4DJ4hcLXtKXNJeoXOIx5melacGovJ9lbCAw1g6OJjWGO/01Kb6w== dependencies: brace "^0.11.1" classnames "^2.2.5" @@ -14139,21 +14153,16 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xmlbuilder@^9.0.7: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== xmlchars@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@0.1.x: - version "0.1.31" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" - integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== - xmlhttprequest@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"