From 23ddee4848de70e48d0b335c05b27e1621422a74 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 28 Jul 2022 08:22:32 +0100 Subject: [PATCH] Moved over circles project --- package.json | 8 +- .../Projects/CircleArt/CircleArt.tsx | 34 + .../Projects/CircleArt/Editor/Editor.css | 22 + .../Projects/CircleArt/Editor/Editor.tsx | 641 ++++++++++++++++++ .../CircleArt/Editor/EditorCircle.tsx | 35 + .../CircleArt/Editor/EditorControls.tsx | 154 +++++ .../CircleArt/Editor/EditorIntersection.tsx | 36 + .../CircleArt/Editor/EditorToolbar.tsx | 73 ++ .../CircleArt/Editor/useEditorHistory.ts | 86 +++ .../Projects/CircleArt/Gallery/Gallery.tsx | 37 + .../CircleArt/Gallery/configurations/Fox.json | 117 ++++ .../CircleArt/Gallery/configurations/Fox.svg | 2 + .../Gallery/configurations/Island.json | 121 ++++ .../Gallery/configurations/Island.svg | 1 + .../Gallery/configurations/Monkey.json | 113 +++ .../Gallery/configurations/Monkey.svg | 2 + .../Gallery/configurations/Mushroom.json | 144 ++++ .../Gallery/configurations/Mushroom.svg | 2 + .../Gallery/configurations/Profile.json | 92 +++ .../Gallery/configurations/Profile.svg | 2 + .../Gallery/configurations/Whale.json | 106 +++ .../Gallery/configurations/Whale.svg | 2 + .../CircleArt/Gallery/configurations/index.ts | 35 + src/components/Projects/CircleArt/types.ts | 16 + .../Projects/CircleArt/utils/cursor.ts | 40 ++ .../Projects/CircleArt/utils/math/atan2.ts | 5 + .../utils/math/isPointOverCircleEdge.ts | 5 + .../utils/math/isPointWithinCircle.ts | 3 + .../GraphVisualisation/GraphVisualisation.tsx | 87 +-- .../GraphVisualisationTraversal.tsx | 4 +- .../GraphVisualisation/getArcPath.ts | 27 + .../GraphVisualisation/getScaledProps.ts | 22 + .../GraphVisualisation/getTraversalPath.ts | 28 + .../IntersectionExplorer.css | 7 +- .../IntersectionExplorer.tsx | 12 +- .../NodeList/NodeList.tsx | 11 +- .../TraversalList/TraversalList.tsx | 10 +- .../IntersectionExplorer/useGraph/circle.ts | 7 +- .../IntersectionExplorer/useGraph/edge.ts | 2 +- .../IntersectionExplorer/useGraph/graph.ts | 57 ++ .../IntersectionExplorer/useGraph/index.ts | 144 ++-- .../IntersectionExplorer/useGraph/node.ts | 2 +- .../useGraph/traversal.ts | 121 ++++ .../IntersectionExplorer/useGraph/validate.ts | 2 +- .../useTraversals/index.ts | 0 src/components/Root.tsx | 2 + .../Writings/CircleGraphs/CircleGraphs.tsx | 2 +- src/data.ts | 8 +- yarn.lock | 23 +- 49 files changed, 2300 insertions(+), 212 deletions(-) create mode 100644 src/components/Projects/CircleArt/CircleArt.tsx create mode 100644 src/components/Projects/CircleArt/Editor/Editor.css create mode 100644 src/components/Projects/CircleArt/Editor/Editor.tsx create mode 100644 src/components/Projects/CircleArt/Editor/EditorCircle.tsx create mode 100644 src/components/Projects/CircleArt/Editor/EditorControls.tsx create mode 100644 src/components/Projects/CircleArt/Editor/EditorIntersection.tsx create mode 100644 src/components/Projects/CircleArt/Editor/EditorToolbar.tsx create mode 100644 src/components/Projects/CircleArt/Editor/useEditorHistory.ts create mode 100644 src/components/Projects/CircleArt/Gallery/Gallery.tsx create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Fox.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Fox.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Island.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Island.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Monkey.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Profile.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Profile.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Whale.json create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/Whale.svg create mode 100644 src/components/Projects/CircleArt/Gallery/configurations/index.ts create mode 100644 src/components/Projects/CircleArt/types.ts create mode 100644 src/components/Projects/CircleArt/utils/cursor.ts create mode 100644 src/components/Projects/CircleArt/utils/math/atan2.ts create mode 100644 src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts create mode 100644 src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts create mode 100644 src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts create mode 100644 src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts create mode 100644 src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts create mode 100644 src/components/Projects/IntersectionExplorer/useGraph/graph.ts delete mode 100644 src/components/Projects/IntersectionExplorer/useTraversals/index.ts diff --git a/package.json b/package.json index 512983d1..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.2", + "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/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..22d490a9 --- /dev/null +++ b/src/components/Projects/CircleArt/Editor/Editor.css @@ -0,0 +1,22 @@ +.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); + } + } +} 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.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/TraversalList/TraversalList.tsx b/src/components/Projects/IntersectionExplorer/TraversalList/TraversalList.tsx index 6493c295..d32fed84 100644 --- a/src/components/Projects/IntersectionExplorer/TraversalList/TraversalList.tsx +++ b/src/components/Projects/IntersectionExplorer/TraversalList/TraversalList.tsx @@ -1,4 +1,4 @@ -import { Labels } from 'preshape'; +import { Labels, Text } from 'preshape'; import React, { FunctionComponent, useContext } from 'react'; import { IntersectionExplorerContext } from '../IntersectionExplorer'; import { getCompleteTraversals } from '../useGraph/traversal'; @@ -9,8 +9,12 @@ interface Props { } const TraversalList: FunctionComponent = ({ 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/Root.tsx b/src/components/Root.tsx index 5a4004d3..bff853a8 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -10,6 +10,7 @@ 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'; @@ -55,6 +56,7 @@ const Site = () => { } /> + } path={data.projects.CircleArt.to} /> } path={data.projects.CircleGraph.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/data.ts b/src/data.ts index dfb5616d..ad884d3f 100644 --- a/src/data.ts +++ b/src/data.ts @@ -2,7 +2,7 @@ import { Data } from './types'; const data: Data< 'Pure360' | 'Reedsy' | 'Brandwatch' | 'Bitrise' | 'Spotify', - 'CircleGraph' | 'Circles' | 'Antwerp' | 'Preshape' | 'Snake' | 'Spirals', + 'CircleGraph' | 'CircleArt' | 'Antwerp' | 'Preshape' | 'Snake' | 'Spirals', | 'CircleGraphs' | 'CircleIntersections' | 'GeneratingTessellations' @@ -58,13 +58,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: diff --git a/yarn.lock b/yarn.lock index 88c00729..8b392cbb 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" @@ -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" @@ -10963,10 +10978,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.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/preshape/-/preshape-12.0.2.tgz#a6642f303d4b1215d74f04e25d6806946c4f89d1" - integrity sha512-JCUxwKtUu+yt0geCHMZZ/CnSUCtIS2b5v/WOQLaALBn9/qULIqegbwdQVnqvrFsZozaoRaLS4AG+IeaAaDOq1A== +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"