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..a7367ded
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/Editor.css
@@ -0,0 +1,68 @@
+.CircleArt__circle,
+.CircleArt__intersection {
+ stroke-width: 1;
+ transition-property: fill, stroke, stroke-width;
+ transition-duration: var(--transition-duration--fast);
+ transition-timing-function: var(--transition-timing-function);
+}
+
+.CircleArt__circle--active {
+ stroke: var(--color-accent-shade-4);
+ stroke-width: 2;
+}
+
+.CircleArt--mode-draw {
+ & .CircleArt__circle {
+ fill: transparent;
+
+ &:hover {
+ stroke: var(--color-accent-shade-4);
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+/* .CircleArt__circle--active,
+.CircleArt__circle:hover {
+ stroke: var(--color-accent-shade-4);
+}
+
+.CircleArt__circle--selectable,
+.CircleArt__intersection--selectable {
+ stroke: var(--color-text-shade-4);
+}
+
+.CircleArt__circle--selectable:hover,
+.CircleArt__intersection--selectable:hover {
+ stroke: var(--color-accent-shade-4);
+} */
+
+
+
+
+
+
+
+.CircleArt--mode-fill,
+.CircleArt--mode-view {
+ /* & .CircleArt__circle--selectable.CircleArt__circle--filled:hover,
+ & .CircleArt__intersection--selectable.CircleArt__intersection--filled:hover {
+ fill: var(--color-text-shade-1);
+ } */
+}
+
+/* .CircleArt--mode-view {
+ & .CircleArt__circle,
+ & .CircleArt__intersection {
+ stroke: rgba(var(--rgb-text-shade-3), 0.25);
+ }
+} */
diff --git a/src/components/Projects/CircleArt/Editor/Editor.tsx b/src/components/Projects/CircleArt/Editor/Editor.tsx
new file mode 100644
index 00000000..8600174d
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/Editor.tsx
@@ -0,0 +1,641 @@
+import classNames from 'classnames';
+import FileSaver from 'file-saver';
+import {
+ Box,
+ themes,
+ TypeTheme,
+ useEventListener,
+ useResizeObserver,
+} from 'preshape';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { v4 } from 'uuid';
+import useGraph, { Circle } from '../../IntersectionExplorer/useGraph';
+import { CircleArtData } from '../types';
+import {
+ CURSOR_DEFAULT,
+ CURSOR_DRAW,
+ CURSOR_FILL,
+ CURSOR_MOVE,
+ getCursor,
+ setCursor,
+} from '../utils/cursor';
+import isPointOverCircleEdge from '../utils/math/isPointOverCircleEdge';
+import isPointWithinCircle from '../utils/math/isPointWithinCircle';
+import EditorCircle from './EditorCircle';
+import EditorControls from './EditorControls';
+import EditorIntersection from './EditorIntersection';
+import EditorToolbar from './EditorToolbar';
+import useEditorHistory from './useEditorHistory';
+import './Editor.css';
+
+export type TypeMode = 'draw' | 'fill' | 'view';
+
+type MinimalEvent = {
+ clientX: number;
+ clientY: number;
+ target: null | EventTarget;
+ type: string;
+};
+
+type Props = {
+ data: CircleArtData;
+};
+
+const COPY_OFFSET = 25;
+const TOLERANCE_CREATE_CIRCLE = 30;
+const TOLERANCE_SELECT_CIRCLE = 3;
+
+const onMouseDownGlobal = () => {
+ document.body.style.userSelect = 'none';
+};
+
+const onMouseUpGlobal = () => {
+ document.body.style.userSelect = '';
+};
+
+const minimalEvent = (event: TouchEvent | React.TouchEvent): MinimalEvent => ({
+ clientX: event.touches[0]?.clientX,
+ clientY: event.touches[0]?.clientY,
+ target: event.target,
+ type: event.type,
+});
+
+export const getColors = (theme: TypeTheme, filled?: boolean) => {
+ return {
+ fill: filled
+ ? themes[theme].colorTextShade1
+ : themes[theme].colorBackgroundShade1,
+ stroke: filled
+ ? themes[theme].colorBackgroundShade1
+ : themes[theme].colorTextShade4,
+ };
+};
+
+const Editor = ({ data }: Props) => {
+ const [{ width, height }, ref] = useResizeObserver();
+
+ const refActiveCircle = useRef(null);
+ const refCirclesContainer = useRef(null);
+ const refContainer = useRef(null);
+ const refIsAdding = useRef(false);
+ const refIsDirty = useRef(false);
+ const refIsMoving = useRef(false);
+ const refIsPointerDown = useRef(false);
+ const refIsResizing = useRef(false);
+ const refPointerPosition = useRef<[number, number]>([-1, -1]);
+ const refQueueActiveUpdate = useRef(false);
+
+ const [activeCircle, setActiveCircle] = useState(null);
+ const [toolbarRect, setToolbarRect] = useState(null);
+ const [fills, setFills] = useState(data.fills);
+ const [mode, setMode] = useState('view');
+
+ const [circles, setCircles] = useState([]);
+ const editorHistory = useEditorHistory();
+
+ const { graph } = useGraph(circles, {
+ findTraversalsOnUpdate: mode === 'fill' || mode === 'view',
+ });
+
+ const getScaledCircles = (circles: Circle[]) => {
+ const scale = Math.min(width / data.width, height / data.height);
+ const tx = (width - data.width * scale) * 0.5;
+ const ty = (height - data.height * scale) * 0.5;
+
+ return circles.map(({ id, radius, x, y }) => ({
+ id,
+ radius: radius * scale,
+ x: x * scale + tx,
+ y: y * scale + ty,
+ }));
+ };
+
+ const handleSaveJSON = () => {
+ FileSaver.saveAs(
+ new Blob(
+ [
+ JSON.stringify(
+ {
+ height,
+ width,
+ circles,
+ fills,
+ },
+ null,
+ 2
+ ),
+ ],
+ {
+ type: 'text/json;charset=utf-8',
+ }
+ ),
+ `CircleArt-export_${Date.now()}.json`
+ );
+ };
+
+ const handleSavePNG = () => {
+ if (refContainer.current) {
+ const serializer = new XMLSerializer();
+ const svgString = serializer.serializeToString(refContainer.current);
+
+ FileSaver.saveAs(
+ new Blob(
+ [
+ `
+${svgString}
+`,
+ ],
+ {
+ type: 'text/svg;charset=utf-8',
+ }
+ ),
+ `CircleArt-export_${Date.now()}.svg`
+ );
+ }
+ };
+
+ const handleClear = () => {
+ setCircles([]);
+ setMode('draw');
+ handleSetActiveCircle(null);
+ handleSetToolbarRect(null);
+ editorHistory.commit();
+ };
+
+ const handleSetMode = (nextMode: TypeMode) => {
+ const currentMode = mode;
+ refIsDirty.current = true;
+ editorHistory.push(
+ () => {
+ setMode(nextMode);
+ handleSetActiveCircle(null);
+ handleSetToolbarRect(null);
+ },
+ {
+ undo: () => {
+ setMode(currentMode);
+ },
+ }
+ );
+ };
+
+ const handleSetToolbarRect = (circle: Circle | null) => {
+ if (circle && refContainer.current) {
+ const { left, top } = refContainer.current.getBoundingClientRect();
+ const { x, y, radius } = circle;
+
+ setToolbarRect(
+ new DOMRect(
+ left + (x - radius),
+ top + (y - radius),
+ radius * 2,
+ radius * 2
+ )
+ );
+ } else {
+ setToolbarRect(null);
+ }
+ };
+
+ const getNewCircle = (
+ x: number,
+ y: number,
+ radius: number = 0,
+ id = v4()
+ ): Circle => ({
+ id,
+ radius,
+ x,
+ y,
+ });
+
+ const getActiveCircleElement = () => {
+ if (refCirclesContainer.current && refActiveCircle.current) {
+ for (const element of refCirclesContainer.current.children) {
+ if (element.id === refActiveCircle.current.id) {
+ return element;
+ }
+ }
+ }
+
+ return null;
+ };
+
+ const getCircleAtCoordinates = useCallback(
+ (x: number, y: number, padding: number) => {
+ for (let i = graph.circles.length - 1; i >= 0; i--) {
+ const { x: cx, y: cy, radius } = graph.circles[i];
+
+ if (isPointWithinCircle(x, y, cx, cy, radius, padding)) {
+ return graph.circles[i];
+ }
+ }
+
+ return null;
+ },
+ [graph.circles]
+ );
+
+ const getRelativeCoordinates = ({ clientX, clientY }: MinimalEvent) => {
+ if (refContainer.current) {
+ const { left, top } = refContainer.current.getBoundingClientRect();
+ const x = clientX - left;
+ const y = clientY - top;
+ const [startX, startY] = refPointerPosition.current;
+ const deltaX = x - startX;
+ const deltaY = y - startY;
+
+ return {
+ deltaX,
+ deltaY,
+ x,
+ y,
+ };
+ }
+
+ return {
+ deltaX: 0,
+ deltaY: 0,
+ x: 0,
+ y: 0,
+ };
+ };
+
+ const handleSetActiveCircle = (circle: Circle | null) => {
+ refActiveCircle.current = circle;
+ setActiveCircle(circle);
+ };
+
+ const handleAddCircle = (circle: Circle) => {
+ setCircles((circles) => [...circles, circle]);
+ handleSetActiveCircle(circle);
+ };
+
+ const handleRemoveCircle = (circle: Circle) => {
+ editorHistory.push(
+ () => {
+ setCircles((circles) => circles.filter(({ id }) => circle.id !== id));
+ handleSetActiveCircle(null);
+ handleSetToolbarRect(null);
+ },
+ {
+ undo: () => {
+ handleAddCircle(circle);
+ },
+ }
+ );
+ };
+
+ const handleUpdateCircle = (update: Circle) => {
+ setCircles((circles) =>
+ circles.map((circle) => (circle.id === update.id ? update : circle))
+ );
+ };
+
+ const handleCopyActiveCircle = () => {
+ if (refActiveCircle.current) {
+ const circle = getNewCircle(
+ refActiveCircle.current.x + COPY_OFFSET,
+ refActiveCircle.current.y + COPY_OFFSET,
+ refActiveCircle.current.radius
+ );
+
+ editorHistory.push(
+ () => {
+ handleAddCircle(circle);
+ handleSetToolbarRect(circle);
+ },
+ {
+ undo: () => handleRemoveCircle(circle),
+ }
+ );
+ }
+ };
+
+ const handleMoveActiveCircle = (deltaX: number, deltaY: number) => {
+ const circle = refActiveCircle.current;
+ const element = getActiveCircleElement();
+
+ if (circle && element) {
+ const x = circle.x + deltaX;
+ const y = circle.y + deltaY;
+
+ element.setAttribute('cx', x.toString());
+ element.setAttribute('cy', y.toString());
+
+ refQueueActiveUpdate.current = true;
+ }
+ };
+
+ const handleRemoveActiveCircle = () => {
+ if (refActiveCircle.current) {
+ handleRemoveCircle(refActiveCircle.current);
+ }
+ };
+
+ const handleResizeActiveCircle = (x: number, y: number) => {
+ const circle = refActiveCircle.current;
+ const element = getActiveCircleElement();
+
+ if (circle && element) {
+ const radius = Math.hypot(x - circle.x, y - circle.y);
+
+ element.setAttribute('r', radius.toString());
+
+ refQueueActiveUpdate.current = true;
+ }
+ };
+
+ const handleToggleFilled = (id: string) => {
+ editorHistory.push(
+ () => {
+ setFills((fills) => ({ ...fills, [id]: !fills[id] }));
+ },
+ {
+ undo: () => {
+ setFills((fills) => ({ ...fills, [id]: !fills[id] }));
+ },
+ }
+ );
+ };
+
+ const handleMouseDown = useCallback(
+ (event: MinimalEvent) => {
+ refIsPointerDown.current = true;
+ onMouseDownGlobal();
+
+ if (mode === 'draw') {
+ const { x, y } = getRelativeCoordinates(event);
+ const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE);
+
+ handleSetActiveCircle(circle);
+
+ if (circle) {
+ refPointerPosition.current = [x, y];
+
+ if (
+ isPointOverCircleEdge(
+ x,
+ y,
+ circle.x,
+ circle.y,
+ circle.radius,
+ TOLERANCE_SELECT_CIRCLE
+ )
+ ) {
+ refIsResizing.current = true;
+ } else {
+ refIsMoving.current = true;
+ }
+ }
+ }
+ },
+ [getCircleAtCoordinates, mode]
+ );
+
+ const handleMouseUp = useCallback(() => {
+ onMouseUpGlobal();
+
+ if (refActiveCircle.current) {
+ if (refQueueActiveUpdate.current) {
+ const element = getActiveCircleElement();
+
+ if (element) {
+ const currentCircle = refActiveCircle.current;
+ const updatedCircle: Circle = {
+ id: currentCircle.id,
+ radius: parseFloat(element.getAttribute('r') || '0'),
+ x: parseFloat(element.getAttribute('cx') || '0'),
+ y: parseFloat(element.getAttribute('cy') || '0'),
+ };
+
+ const update = (circle: Circle) => {
+ handleUpdateCircle(circle);
+ handleSetActiveCircle(circle);
+ handleSetToolbarRect(circle);
+ };
+
+ if (updatedCircle.radius < TOLERANCE_CREATE_CIRCLE) {
+ handleRemoveActiveCircle();
+ } else if (refIsAdding.current) {
+ editorHistory.push(() => update(updatedCircle), {
+ undo: () => handleRemoveCircle(updatedCircle),
+ redo: () => {
+ handleAddCircle(updatedCircle);
+ handleSetToolbarRect(updatedCircle);
+ },
+ });
+ } else {
+ editorHistory.push(() => update(updatedCircle), {
+ undo: () => update(currentCircle),
+ });
+ }
+ }
+ } else {
+ handleSetToolbarRect(refActiveCircle.current);
+ }
+ } else {
+ handleSetToolbarRect(null);
+ }
+
+ refIsAdding.current = false;
+ refIsPointerDown.current = false;
+ refIsMoving.current = false;
+ refIsResizing.current = false;
+ }, [handleUpdateCircle, mode]);
+
+ const handleTouchEnd = useCallback(
+ (event: TouchEvent) => {
+ handleMouseUp();
+
+ if (refContainer.current?.contains(event.target as Node)) {
+ event.preventDefault();
+ }
+ },
+ [handleMouseUp]
+ );
+
+ const handleDrag = useCallback(
+ (event: MinimalEvent) => {
+ if (mode === 'draw') {
+ const { deltaX, deltaY, x, y } = getRelativeCoordinates(event);
+
+ if (refActiveCircle.current) {
+ setToolbarRect(null);
+
+ if (refIsResizing.current) {
+ handleResizeActiveCircle(x, y);
+ } else {
+ handleMoveActiveCircle(deltaX, deltaY);
+ }
+ } else if (Math.hypot(deltaX, deltaY) > TOLERANCE_CREATE_CIRCLE) {
+ handleAddCircle(getNewCircle(x, y));
+ refIsAdding.current = true;
+ refIsResizing.current = true;
+ }
+ }
+ },
+ [mode]
+ );
+
+ const handleMouseMoveDraw = useCallback(
+ (event: MinimalEvent) => {
+ const { x, y } = getRelativeCoordinates(event);
+ const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE);
+
+ if (circle) {
+ if (
+ isPointOverCircleEdge(
+ x,
+ y,
+ circle.x,
+ circle.y,
+ circle.radius,
+ TOLERANCE_SELECT_CIRCLE
+ )
+ ) {
+ setCursor(refContainer.current, getCursor(x, y, circle.x, circle.y));
+ } else {
+ setCursor(refContainer.current, CURSOR_MOVE);
+ }
+ } else {
+ setCursor(refContainer.current, CURSOR_DRAW);
+ }
+ },
+ [getCircleAtCoordinates]
+ );
+
+ const handleMouseMoveFill = useCallback(
+ (event: MinimalEvent) => {
+ const { x, y } = getRelativeCoordinates(event);
+ const circle = getCircleAtCoordinates(x, y, TOLERANCE_SELECT_CIRCLE);
+
+ if (circle) {
+ setCursor(refContainer.current, CURSOR_FILL);
+ } else {
+ setCursor(refContainer.current, CURSOR_DEFAULT);
+ }
+ },
+ [getCircleAtCoordinates]
+ );
+
+ const handleMouseMove = useCallback(
+ (event: MinimalEvent) => {
+ if (refIsPointerDown.current) {
+ return handleDrag(event);
+ }
+
+ if (mode === 'draw') handleMouseMoveDraw(event);
+ if (mode === 'fill') handleMouseMoveFill(event);
+ },
+ [handleDrag, handleMouseMoveDraw, handleMouseMoveFill, mode]
+ );
+
+ useEffect(() => {
+ if (width && height) {
+ setCircles(getScaledCircles(data.circles));
+ setFills(data.fills);
+ }
+ }, [data, width, height]);
+
+ useEventListener(document, 'mouseup', handleMouseUp);
+ useEventListener(document, 'mousemove', handleMouseMove);
+ useEventListener(document, 'touchend', handleTouchEnd);
+
+ const classes = classNames('CircleArt', `CircleArt--mode-${mode}`);
+
+ return (
+
+
+ handleMouseMove(minimalEvent(e))}
+ onTouchStart={(e) => handleMouseDown(minimalEvent(e))}
+ preserveAspectRatio="xMidYMid meet"
+ ref={refContainer}
+ tag="svg"
+ viewBox={`0 0 ${width} ${height}`}
+ width={width}
+ >
+ {mode === 'draw' && (
+
+ {graph.circles
+ .sort((a, b) => b.radius - a.radius)
+ .map(({ id, radius, x, y }) => (
+
+ ))}
+
+ )}
+
+ {(mode === 'fill' || mode === 'view') && (
+
+ {graph.traversals.map((traversal) => (
+ handleToggleFilled(traversal.bitset.toString())
+ : undefined
+ }
+ traversal={traversal}
+ />
+ ))}
+
+ {graph.circles
+ .filter((_, i) =>
+ graph.edges.every(({ circle }) => circle !== i)
+ )
+ .map(({ id, radius, x, y }) => (
+ handleToggleFilled(id)
+ : undefined
+ }
+ radius={radius}
+ x={x}
+ y={y}
+ />
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default Editor;
diff --git a/src/components/Projects/CircleArt/Editor/EditorCircle.tsx b/src/components/Projects/CircleArt/Editor/EditorCircle.tsx
new file mode 100644
index 00000000..ea0e848b
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/EditorCircle.tsx
@@ -0,0 +1,35 @@
+import classNames from 'classnames';
+import { Box } from 'preshape';
+import React, { useContext } from 'react';
+import { RootContext } from '../../../Root';
+import { Circle } from '../../IntersectionExplorer/useGraph';
+import { getColors } from './Editor';
+
+type Props = Circle & {
+ active?: boolean;
+ filled?: boolean;
+ onClick?: () => void;
+};
+
+const EditorCircle = ({ active, filled, id, onClick, radius, x, y }: Props) => {
+ const { theme } = useContext(RootContext);
+ const classes = classNames('CircleArt__circle', {
+ 'CircleArt__circle--active': active,
+ 'CircleArt__circle--selectable': onClick,
+ });
+
+ return (
+
+ );
+};
+
+export default EditorCircle;
diff --git a/src/components/Projects/CircleArt/Editor/EditorControls.tsx b/src/components/Projects/CircleArt/Editor/EditorControls.tsx
new file mode 100644
index 00000000..971e9b02
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/EditorControls.tsx
@@ -0,0 +1,154 @@
+import { useMatchMedia, Button, Buttons, Box, Icons } from 'preshape';
+import React from 'react';
+import { TypeMode } from '../types';
+
+const canSave = typeof window !== 'undefined' && window.Blob !== undefined;
+
+interface Props {
+ canRedo: boolean;
+ canUndo: boolean;
+ mode: TypeMode;
+ onChangeMode: (mode: TypeMode) => void;
+ onClear: () => void;
+ onRedo: () => void;
+ onSaveJSON: () => void;
+ onSavePNG: () => void;
+ onUndo: () => void;
+}
+
+const EditorControls = (props: Props) => {
+ const match = useMatchMedia(['600px', '800px']);
+ const {
+ canUndo,
+ canRedo,
+ mode,
+ onChangeMode,
+ onClear,
+ onRedo,
+ onSaveJSON,
+ onSavePNG,
+ onUndo,
+ } = props;
+
+ return (
+ event.nativeEvent.stopPropagation()}
+ padding="x6"
+ width="100%"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default EditorControls;
diff --git a/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx b/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx
new file mode 100644
index 00000000..78d1e0a7
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/EditorIntersection.tsx
@@ -0,0 +1,36 @@
+import classNames from 'classnames';
+import { Box } from 'preshape';
+import React, { useContext } from 'react';
+import { RootContext } from '../../../Root';
+import getTraversalPath from '../../IntersectionExplorer/GraphVisualisation/getTraversalPath';
+import { Graph, Traversal } from '../../IntersectionExplorer/useGraph';
+import { getColors } from './Editor';
+
+type Props = {
+ filled?: boolean;
+ graph: Graph;
+ onClick?: () => void;
+ traversal: Traversal;
+};
+
+const EditorIntersection = ({ filled, graph, onClick, traversal }: Props) => {
+ const { theme } = useContext(RootContext);
+ const classes = classNames('CircleArt__intersection', {
+ 'CircleArt__intersection--selectable': onClick,
+ });
+
+ const d = getTraversalPath(traversal, graph.nodes, graph.edges);
+
+ return (
+
+ );
+};
+
+export default EditorIntersection;
diff --git a/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx b/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx
new file mode 100644
index 00000000..5c017c74
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/EditorToolbar.tsx
@@ -0,0 +1,73 @@
+import { ReferenceObject } from 'popper.js';
+import {
+ Button,
+ Buttons,
+ Icons,
+ Placement,
+ PlacementArrow,
+ PlacementContent,
+} from 'preshape';
+import React, { useEffect, useState } from 'react';
+
+interface Props {
+ rect: DOMRect | null;
+ onCopy: () => void;
+ onDelete: () => void;
+}
+
+class Reference implements ReferenceObject {
+ rect: DOMRect;
+
+ constructor(rect: DOMRect) {
+ this.rect = rect;
+ }
+
+ get clientHeight() {
+ return this.rect.height;
+ }
+
+ get clientWidth() {
+ return this.rect.width;
+ }
+
+ getBoundingClientRect(): DOMRect {
+ return this.rect;
+ }
+}
+
+export default function EditorToolbar({ rect, onCopy, onDelete }: Props) {
+ const [referenceElement, setReferenceElement] = useState();
+
+ useEffect(() => {
+ if (rect) {
+ setReferenceElement(new Reference(rect));
+ }
+ }, [rect]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Projects/CircleArt/Editor/useEditorHistory.ts b/src/components/Projects/CircleArt/Editor/useEditorHistory.ts
new file mode 100644
index 00000000..ff2a170c
--- /dev/null
+++ b/src/components/Projects/CircleArt/Editor/useEditorHistory.ts
@@ -0,0 +1,86 @@
+import { useRef, useState } from "react";
+
+interface Action {
+ undo: () => void;
+ redo?: () => void;
+}
+
+interface Store {
+ history: Action[];
+ future: Action[];
+}
+
+const useEditorHistory = () => {
+ const refAllowPush = useRef(true);
+ const [store, setStore] = useState({
+ history: [],
+ future: [],
+ });
+
+ const commit = () => {
+ setStore({
+ history: [],
+ future: [],
+ })
+ };
+
+ const push = (action: () => void, { undo, redo = action }: Action) => {
+ action();
+
+ if (refAllowPush.current) {
+ setStore((store) => ({
+ history: [...store?.history, { undo, redo }],
+ future: [],
+ }));
+ }
+ };
+
+ const pop = () => {
+ setStore((store) => {
+ const action = store.history[store.history.length - 1];
+
+ if (action) {
+ refAllowPush.current = false;
+ action.undo();
+ refAllowPush.current = true;
+
+ return {
+ history: store.history.slice(0, -1),
+ future: [...store.future, action],
+ };
+ }
+
+ return store;
+ });
+ };
+
+ const replay = () => {
+ setStore((store) => {
+ const action = store.future[store.future.length - 1];
+
+ if (action) {
+ refAllowPush.current = false;
+ action.redo?.();
+ refAllowPush.current = true;
+
+ return {
+ history: [...store.history, action],
+ future: store.future.slice(0, -1),
+ };
+ }
+
+ return store;
+ });
+ };
+
+ return {
+ canUndo: !!store.history.length,
+ canRedo: !!store.future.length,
+ commit,
+ push,
+ pop,
+ replay,
+ };
+};
+
+export default useEditorHistory;
diff --git a/src/components/Projects/CircleArt/Gallery/Gallery.tsx b/src/components/Projects/CircleArt/Gallery/Gallery.tsx
new file mode 100644
index 00000000..b6b4e015
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/Gallery.tsx
@@ -0,0 +1,37 @@
+import { Button, Grid, Image } from 'preshape';
+import React from 'react';
+import { CircleArtGalleryItem } from '../types';
+import configurations from './configurations';
+
+const Gallery = ({
+ onSelect,
+}: {
+ onSelect: (data: CircleArtGalleryItem) => void;
+}) => {
+ return (
+
+ {configurations.map((data) => (
+
+ ))}
+
+ );
+};
+
+export default Gallery;
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Fox.json b/src/components/Projects/CircleArt/Gallery/configurations/Fox.json
new file mode 100644
index 00000000..7fdd718c
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Fox.json
@@ -0,0 +1,117 @@
+{
+ "height": 774,
+ "width": 1584,
+ "circles": [
+ {
+ "id": "ee54790d-d2e9-4c9e-972d-e3925352f79f",
+ "radius": 358.0348463487878,
+ "x": 1111.92,
+ "y": 399.9000000000001
+ },
+ {
+ "id": "a61260ca-57f2-4ade-b436-ce039f746fa9",
+ "radius": 191.35531453293902,
+ "x": 793.2899999999998,
+ "y": 316.05
+ },
+ {
+ "id": "3200307b-1b83-49dc-b534-b725d22d1d61",
+ "radius": 188.33116417629876,
+ "x": 790.71,
+ "y": 332.82000000000005
+ },
+ {
+ "id": "cf1e907e-e3fb-43fc-9dc9-13908b7e0ada",
+ "radius": 183.5656855188355,
+ "x": 790.71,
+ "y": 468.27000000000004
+ },
+ {
+ "id": "ccc62704-3292-4d97-a2b1-c16263e76279",
+ "radius": 182.17796354114842,
+ "x": 802.32,
+ "y": 319.92
+ },
+ {
+ "id": "69032566-2f78-4c25-8bf4-9d0846cc9aa7",
+ "radius": 179.3285601905062,
+ "x": 806.19,
+ "y": 145.77
+ },
+ {
+ "id": "633dc374-1b10-4e60-a947-6a9cb3bb5035",
+ "radius": 173.78172170858477,
+ "x": 794.58,
+ "y": 461.82000000000005
+ },
+ {
+ "id": "e7e1d120-f468-4b86-a0a9-8b945ad0d35f",
+ "radius": 149.3060065770966,
+ "x": 865.5300000000001,
+ "y": 438.6
+ },
+ {
+ "id": "e223d3c4-34ee-430f-99f8-1ae752fe1256",
+ "radius": 149.0215236132016,
+ "x": 839.73,
+ "y": 712.08
+ },
+ {
+ "id": "c700ab07-2d3c-4b32-b1d2-0ba1a3a69cc6",
+ "radius": 79.06973947092531,
+ "x": 942.9299999999998,
+ "y": 175.44000000000003
+ },
+ {
+ "id": "dcb54c82-fdd6-412b-b405-abe39b5c4d8c",
+ "radius": 30.44509320071134,
+ "x": 870.6900000000003,
+ "y": 224.46
+ },
+ {
+ "id": "f607fad9-31d0-4a2f-ac36-c3933290fcec",
+ "radius": 30.44509320071134,
+ "x": 864.24,
+ "y": 273.48
+ }
+ ],
+ "fills": {
+ "1000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000100010000000000000000000000000000010100000000000000000000000000000000000000": false,
+ "100000000000000000000000000000001000000000000000000000000000000000000000000000001000000000001000000000000000000000000000000000000000000000000000010000000000000000000100000000000100000100000000000000000000000000000000": true,
+ "1000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000000010100000000010100000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000010000000000001000000000000001000000000000000000000000000000000000000000000000000000000000000000000100000100000100000100000000000000000000": true,
+ "100000000000000000000000000000000100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000010000000000000000000100000000000000000000000000000000000000100000100000": true,
+ "1000000000000000010000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000100010000000000000000000000000000000000000000000000000000000010100000000000": true,
+ "10000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000100000000000100000000000000000000000000000000010000000000000000000000000100000000001000": true,
+ "1000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000010000000000000000001000000000000000000000000000000000000000000000010000000000010000000000000000000000001010": true,
+ "1000000000000000000001000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000001000100000000000000000000000000000000000000000000000000000000010100000000000": true,
+ "10000000000000000000000000000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000100000000000100000000000000000001000000000000000000000000000000000000000100000100000": true,
+ "10000000000000000000000010000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100100000000000000001001000000000000000000000000000000000000000000000000": true,
+ "1000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000100100000000000000000000000000000000000000000000000000000000000000000000": true,
+ "100000000000000000000101000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000100000000001000000100000000000000000000000000001000000000000000000000000100000000001000": true,
+ "11000000000000000000000000000000000000000000000001000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000001000000001000001000000000000000000000000000000000": true,
+ "100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000001000000000000000000000000000000000000000000000000000000000000110000000000000": true,
+ "100000000000000000000000000000000000000000100000000000000000000000000000001000000000000001000000000000000001000000000000000000000000000001000000000000000001000000000000000010000000000000000001010": true,
+ "1000000000000000000000000000000000000000000000001000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000010000010": true,
+ "1000000000001000000000000000000000100000000000000001000000000000000000000000000000000000000000000000100000000000000100000000000000000000000011000000000000000000000000000000000001000010000000001000010000000": true,
+ "1000000000000000000000010000000000000000000000001000000000000000000000000000000000000000010000000000000000000000000000000000000001000000000000000010000000000000000001000001000000000000000000000": true,
+ "1000000000000000000000000000000000010000000000000101000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011001000011000000000000000000000000000000000000000000000000000000000000000": true,
+ "1000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000010000000000000000000000000000000000000000000000000001001000000000": true,
+ "10000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000001000010000000000000000000000000000000000000000000000000000011000000000": true,
+ "100000000000000000000000000000000000100000000000000000100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000100000000000000100000001000000000000000100000000000000000000000000000000000000000010000010000": true,
+ "1000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000010000000000100000000000010000010000000000000000000000000000000000000000000000": true,
+ "100000000001000000001000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000001010010000000001100000000000000000000000000000000000000000000000000000000000000000": true,
+ "101000000010000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001100010000000000000000000000100010000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000010010000000000000000000000000000000000000000000000000": true,
+ "10100000000000000000000000000000001000000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000100011000000000000000000010010000000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000001000000001000000000010000010000000000000000000000000000000000000000000000": true,
+ "1000000100000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000100000000000000000000000000000000001000000000000101000000000000000000000000100000000010000000000000000000000000000000000000010100": true,
+ "1000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000010000000000000000000000000000000010000000000000000010000000000000000000000000000000000001000100": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000010000000000000000100000000000000000000000000000000010000000000000000000000000000010000000000000000000000001000001": true,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000000000000000000000000000000000000000010000000000000100000000000000001": true,
+ "10000000101000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100010001000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000010000100000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000100001000001000000000000000000000000000000000": true
+ }
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg b/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg
new file mode 100644
index 00000000..62ea7f07
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Fox.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Island.json b/src/components/Projects/CircleArt/Gallery/configurations/Island.json
new file mode 100644
index 00000000..eff61910
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Island.json
@@ -0,0 +1,121 @@
+{
+ "height": 784,
+ "width": 1004,
+ "circles": [
+ {
+ "id": "2852ff49-f1fb-4167-8e98-2023db8f93cd",
+ "radius": 501.8756811092652,
+ "x": 894.6533333333334,
+ "y": 104.5333333333333
+ },
+ {
+ "id": "591de2b9-3e33-43c5-8bc8-b3c1a2eabe5c",
+ "radius": 501.8756811092652,
+ "x": 139.40000000000003,
+ "y": 240.42666666666665
+ },
+ {
+ "id": "4543bb1f-c8a6-4d68-9cba-5a67dc4e6a67",
+ "radius": 501.8756811092652,
+ "x": 127.63999999999999,
+ "y": 186.85333333333338
+ },
+ {
+ "id": "0d4c6dfa-4a13-41ce-8225-d221d80d245c",
+ "radius": 501.8756811092652,
+ "x": 890.7333333333333,
+ "y": 185.54666666666668
+ },
+ {
+ "id": "990b49c0-6eff-4e07-9f34-b7bc2ac06c15",
+ "radius": 224.99722467424152,
+ "x": 505.26666666666665,
+ "y": 659.8666666666667
+ },
+ {
+ "id": "e11238af-ba30-4671-930f-2500116bdf7f",
+ "radius": 180.6416454998373,
+ "x": 502.65333333333325,
+ "y": 593.2266666666667
+ },
+ {
+ "id": "4d3567a4-92a2-473e-94c0-000067ca7363",
+ "radius": 143.82833224214053,
+ "x": 442.5466666666667,
+ "y": 354.1066666666667
+ },
+ {
+ "id": "925a9720-2ede-424e-83ba-cff156993f47",
+ "radius": 143.82833224214053,
+ "x": 540.5466666666667,
+ "y": 355.4133333333333
+ },
+ {
+ "id": "9f1f70fa-c2c2-4f82-8356-7c56291f8bb5",
+ "radius": 143.82833224214053,
+ "x": 660.76,
+ "y": 395.92
+ },
+ {
+ "id": "2e10d006-252c-4877-8927-144df6b1d640",
+ "radius": 143.82833224214053,
+ "x": 368.0666666666667,
+ "y": 350.18666666666667
+ },
+ {
+ "id": "27deddad-8512-449c-93dc-8953406ae242",
+ "radius": 143.82833224214053,
+ "x": 510.49333333333345,
+ "y": 287.46666666666664
+ },
+ {
+ "id": "492793ee-8f36-451b-aef8-d909dcdc2e1c",
+ "radius": 123.27068679220626,
+ "x": 601.96,
+ "y": 373.7066666666667
+ },
+ {
+ "id": "3d61e795-512c-46ab-95b8-8f4c33efa1a4",
+ "radius": 115.94043834850913,
+ "x": 611.1066666666668,
+ "y": 348.88
+ },
+ {
+ "id": "8b400b49-2250-4f8e-be34-d7b9c96ff4a2",
+ "radius": 113.32650018616319,
+ "x": 402.04,
+ "y": 313.6
+ },
+ {
+ "id": "979174bc-f4e7-47e1-98fe-b7183f51af2e",
+ "radius": 113.32650018616319,
+ "x": 438.62666666666667,
+ "y": 292.6933333333333
+ },
+ {
+ "id": "35cc5f6f-50ed-460f-977b-612f22e1b08a",
+ "radius": 80.09131233230676,
+ "x": 349.7733333333333,
+ "y": 266.56
+ },
+ {
+ "id": "533bb888-842d-4c04-91a2-e2e00cc3680d",
+ "radius": 63.71926204497001,
+ "x": 450.3866666666667,
+ "y": 209.06666666666666
+ },
+ {
+ "id": "df4cf2af-6650-48e3-a831-0b298be07269",
+ "radius": 63.71926204497001,
+ "x": 579.7466666666667,
+ "y": 270.47999999999996
+ },
+ {
+ "id": "2667bac5-438b-4fc6-a800-fd14a78d869e",
+ "radius": 63.71926204497001,
+ "x": 672.52,
+ "y": 297.92
+ }
+ ],
+ "fills": {}
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Island.svg b/src/components/Projects/CircleArt/Gallery/configurations/Island.svg
new file mode 100644
index 00000000..474a86f5
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Island.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json
new file mode 100644
index 00000000..baf82f46
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.json
@@ -0,0 +1,113 @@
+{
+ "height": 774,
+ "width": 1584,
+ "circles": [
+ {
+ "id": "35c1f1a3-e368-4710-9a20-44d34f95de81",
+ "radius": 396.45836767559854,
+ "x": 434.66999999999996,
+ "y": 430.85999999999996
+ },
+ {
+ "id": "0f2557b2-765b-4046-951d-5f98cb021887",
+ "radius": 187.49429991335737,
+ "x": 795.87,
+ "y": 433.44000000000005
+ },
+ {
+ "id": "9b8d8361-e3de-49e6-bb86-9bd0ab8b2998",
+ "radius": 160.58298041822488,
+ "x": 782.9699999999999,
+ "y": 419.24999999999994
+ },
+ {
+ "id": "1ed1e95c-8dda-4cae-979e-8bb76b80bd4f",
+ "radius": 160.0432041668749,
+ "x": 799.74,
+ "y": 399.90000000000003
+ },
+ {
+ "id": "eda3812c-0914-4877-a97c-500e45fe0288",
+ "radius": 83.22249816005284,
+ "x": 797.16,
+ "y": 250.26
+ },
+ {
+ "id": "8a2b227a-c023-415a-a8b0-6e568d975cc5",
+ "radius": 70.1005513530386,
+ "x": 797.16,
+ "y": 250.26
+ },
+ {
+ "id": "0dc8128d-a9e9-42d9-b08c-b379f9a5661c",
+ "radius": 36.14302837339451,
+ "x": 762.3299999999999,
+ "y": 248.97
+ },
+ {
+ "id": "c6479e94-eb57-470a-bf31-16d51816d793",
+ "radius": 36.14302837339451,
+ "x": 830.7,
+ "y": 248.97
+ },
+ {
+ "id": "5edefc6f-5199-40b3-bbe3-2bf1de104d36",
+ "radius": 27.21257981155039,
+ "x": 882.3,
+ "y": 247.68
+ },
+ {
+ "id": "ca14669b-5e67-4e2c-b6c0-041296553c8d",
+ "radius": 27.21257981155039,
+ "x": 710.7299999999999,
+ "y": 247.68
+ },
+ {
+ "id": "b85b57f8-b3bd-4f40-b521-cc26868e690a",
+ "radius": 8.260030266288375,
+ "x": 768.78,
+ "y": 254.12999999999997
+ },
+ {
+ "id": "28e1cd33-06ea-4ec9-9268-34cbe0099154",
+ "radius": 8.260030266288375,
+ "x": 825.54,
+ "y": 254.12999999999997
+ }
+ ],
+ "fills": {
+ "100000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000000000000000010000000000000000000000000010000000000001000000000000000000010000000000010000000000000": true,
+ "1000000000000000000000000000000000000000000000100000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000010000010000000000000000000000000000000000000000000000001010000000": true,
+ "1000000001000000000001000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000100010001000000000000000000000000000000000000000000000000000101000000000": true,
+ "10000000000000000000000000000000000100000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000100010000000000000000000000000000000000000000000000000000101000000000": true,
+ "10000000000000000000000000000000001000000000000010000000000000000000000000000000000000000000000000000000000000000000000001000000000001000001000000000000000000000000000000000000000000000001010000000": true,
+ "1000000010000000000000000000000010000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000001000000000000100010000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000100000000000001000000000000000000000000000000000000000000000000000000000000000000000000000001000010000000100000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000010000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000000000001000001000000000010100000000000000000000000000000000000000000000": true,
+ "1000000000001000100000000000000000000000000000000010000000000000000000000000000000000000000000000000000010100000000000000000000000000010100000000000000": true,
+ "10000000000000000000000000000000000000010000000000010000000000000000000000000000000000000010000000000000000000000000000000000000100000100000000000000000000000000000000000000010100000000000000": true,
+ "10000000000000000000000000000000000000100000000000100000000000000000000010000000000000000000000000000000000000000000000000000000100000100000000000000000000010100000000000000000000000000000000": true,
+ "1000000000000000000000000000000000000000000000000010000000000000000000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000010000000100000000000000": true,
+ "100000000000000000000000000000000000000000000000001001000000000000000000000000000000000100000000000000000000000000000000000000000000000010000000100000000000000000000010000000100000000000000": true,
+ "1000000000001000000000000000000000010000000000000000000000000000000000010000000000000000000000000000000000000000000000010100000000000000000000000101000000": true,
+ "10000000000010000000000000000000000000000000000000000000100000000000000000000000000001000000000000000000000000000000000000000000010100000000000000000000000010100000000": false,
+ "10000000000000000000000000000000000000000000000010000000000010000000000000000000000000000000010000000000000000000000000010000000000000000000000100000000000000000010000100000000000000000000000000101000000": true,
+ "10000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000001000000000000000000000000000000000000000000000000100000000000000000010000001000000000000000000000000000000000": true,
+ "1000000000000000000000000000000010000000000001000000000001000000000000000000000000000000000001000000000000000000001000000000000000000000100010000000000000000000010000001000000000000010001000000000000000": true,
+ "1000000000000000000000000000000000001000000000010000000000000100000000000000000000100000000000000000000000000000000000000000000000000000000000010001010000000010001000000000000000000000000000000000000000000000": true,
+ "1000000000000000000000000000000000000000000000000000000000001000000000000000000001000000000000000100000000000000000000100000000000000000000000000000010000000000000100000001000000000000000001010000000000000": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000010000000000000000000000000000010000000000000000000000000000010000000000010000000000000": true,
+ "1010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000010000000000000000000000000000000000000000000000011000000000000001100000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000": true,
+ "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000": true,
+ "100000000000001000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000101000000000001000000000000000000000001000000000000000000000000001101000000000000000000100010": false,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000001000000000000000000001000000000010000000000000000010000000000000000000000010000010000000000000000000000101": true,
+ "100000000000000000000000000000000110000000000000000000000000000010000000000000000000000000000000000000000001000000000000000001000000000101": true,
+ "1000000000000000000000000000000000000000000000000100100000000000000000100000000000000001000000000000000000000000000000000000000000000100000000000000000000000101000000010000000001000000000000": true,
+ "1000000000000000000001100000000000000000000000000000000000100000000000000000000000000000000000000000000000000101000000000000000000000001010000": true,
+ "1000000000000000100000000000000000000000000000001000000000000000000000000000000000000000001000000000000000000000000010100": false,
+ "100000000000000000000000010000000000000000000100000000000000000000000000000000000001000000000000000000000000000000000000010001000000000000000000000000010001000000000000000": true
+ }
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg
new file mode 100644
index 00000000..10882ea5
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Monkey.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json
new file mode 100644
index 00000000..8d3031c9
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.json
@@ -0,0 +1,144 @@
+{
+ "height": 774,
+ "width": 1584,
+ "circles": [
+ {
+ "id": "1df7d3a3-4c0d-45f3-8b9b-87d13bbf3f8b",
+ "radius": 360.84276977653303,
+ "x": 796.515,
+ "y": 7.740000000000031
+ },
+ {
+ "id": "08ff5e5c-cb6d-44ec-90f6-340c0ffffc73",
+ "radius": 337.1740199066352,
+ "x": 886.8149999999999,
+ "y": 492.78000000000003
+ },
+ {
+ "id": "1d747159-7574-4ab1-b953-a0cbfac1a642",
+ "radius": 337.1740199066352,
+ "x": 698.475,
+ "y": 487.62
+ },
+ {
+ "id": "ef949e75-d9a4-4392-b0c7-75697ade97bb",
+ "radius": 267.57784381372085,
+ "x": 796.515,
+ "y": 546.96
+ },
+ {
+ "id": "ee574b9a-1a09-4fb4-b900-81f38fde503d",
+ "radius": 267.57784381372085,
+ "x": 797.8049999999998,
+ "y": 86.43000000000002
+ },
+ {
+ "id": "928bdb76-8588-42b5-b0e7-5e705bc59c97",
+ "radius": 240.5945105358807,
+ "x": 1076.4449999999997,
+ "y": 381.84000000000003
+ },
+ {
+ "id": "5388d1bd-df59-4cbb-bb00-f21fefde44f4",
+ "radius": 240.5945105358807,
+ "x": 516.585,
+ "y": 380.54999999999995
+ },
+ {
+ "id": "b686b11a-2510-447c-9868-7d33a5e4820c",
+ "radius": 136.33780473515043,
+ "x": 796.515,
+ "y": 310.89
+ },
+ {
+ "id": "7030d33d-1afe-4275-ada4-39c73abb052f",
+ "radius": 114.92589655947873,
+ "x": 793.935,
+ "y": 443.76
+ },
+ {
+ "id": "5cd70382-7abe-44c4-9f01-155c355f974c",
+ "radius": 50.72177638845075,
+ "x": 796.515,
+ "y": 344.43
+ },
+ {
+ "id": "335db4a2-2676-47ae-ab0b-28507f5d5eee",
+ "radius": 45.150000000000006,
+ "x": 922.9349999999998,
+ "y": 297.99
+ },
+ {
+ "id": "c7e3d719-5b00-453d-9766-2ff4c399d9d1",
+ "radius": 44.12478895133664,
+ "x": 667.5149999999999,
+ "y": 296.70000000000005
+ },
+ {
+ "id": "ead0d0d9-0284-4588-afe3-6bf8ea775419",
+ "radius": 38.20229966899898,
+ "x": 760.395,
+ "y": 513.42
+ },
+ {
+ "id": "cc02c054-6f3d-4142-a975-e6c513b09961",
+ "radius": 37.41,
+ "x": 831.345,
+ "y": 512.13
+ },
+ {
+ "id": "b6203c37-baf0-475b-b585-44945f73f604",
+ "radius": 16.317352726468837,
+ "x": 897.135,
+ "y": 316.05
+ },
+ {
+ "id": "d04a8040-6c52-4665-bd95-0f860d302a88",
+ "radius": 15.746456744296477,
+ "x": 694.605,
+ "y": 316.05
+ }
+ ],
+ "fills": {
+ "10101000000000000000000010000000000000000100000000000000000000000000000000000000000100000000000000000001000000000000010000000000000000000000000000000000000000000000000000100000000100000000000000000000000001100000110000000000110000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000000000100000000000000000010000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000010000000000100000000000000000000000000001000001000000000000000000000000000000000000000000000000": true,
+ "1000000000000000000100000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000100000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000010000110001000000001000000000000000000000000000000000000000000000": true,
+ "10100000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000010000000000000000000001000000010000000000000000000000000000000011000000000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000000100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000010000000000100000000000000000000000000000000000000000000000000001000100000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000001000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010000000010001000000000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000010000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000001000001000000000000000000000000000000000000000000000": true,
+ "10000100000000000000000000000000000000000000000000000000000000000000000010000000000000000000000100000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000100000010100001000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000010000010000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100010010001000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000000000001000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000100000010000000000000000000000000000000000000000000000000000000000000000000001001000000": true,
+ "100000000000000000000000010000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000001001000000000000000000000000000000000000000000000000001001000000": true,
+ "1000000000000000000000000000000000000000000100000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000100001000000000000000000000000000000000000000000000000100001000000000": true,
+ "100000000000000000000000000000000100000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000100000000000000000000010000001000000000000000000000000000000000000000000000000000001010000000000": true,
+ "1000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000101000000000000000000000000000000000000000000000000000101000000000000": true,
+ "1000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000010001000000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000010000000000000000000000000000000100000000100000000000000000000000000000000000000000000000001000000000000000010000000000000000000000000000000100000000000000000010001": false,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000010000000100000000000000000000000": false,
+ "100100000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000001010000000000000": true,
+ "100000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000001000000001000000000000000000000000000000000000000000001000000001000000": true,
+ "1000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000010100000000000": false,
+ "1000000000000000100000000000000000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000100000000000000000000010010000000000000000000000000000000000000000000000000000010100000000000": true,
+ "1000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000100000000000000000000000000000000000000000000000000000000000000000": true,
+ "101000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000010000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000000000000100000000000010000000100000000000000000000000000110000000000000000": true,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000010000000100000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100001000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000000000100000000000000000010000000000000000010000000000000000000000000000101000": false,
+ "10100000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000010000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000100000000000000000000100000111000000000000000000000000000000000000000000": true,
+ "1000000000000000000000000000010000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000001100100100000000000000000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000100000000000000000000100000100000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001000000000000000000100000000000000000000000000000010000010000000000000000000000000000000000000": true,
+ "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000001000000010000000000000000000000000000000000": false,
+ "10000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000010010000000000000000000000000000000000000": true,
+ "1000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000100100000000000000000000000000": true,
+ "1000101010000000000000000000000000000000000000000000010000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000001100000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "1010101000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000110000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "100000010000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "1001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "100000000000000000000000000000000010000000000000000000000100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000010000000000001001000000000000000000000000000000000000000000000000000000000000": true,
+ "1000000000000100000000000000000000000000000000001000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010000001010000000000000000000000000000000000000000000000000000000000000000000000000000": true,
+ "1000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000010000000000000000000000000100000000000000000000000000000000000000010000000000000000000000000000000000000000000010000010000": true,
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000010000000001000000000000000000": false
+ }
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg
new file mode 100644
index 00000000..c4f34c0f
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Mushroom.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Profile.json b/src/components/Projects/CircleArt/Gallery/configurations/Profile.json
new file mode 100644
index 00000000..dbaa7cdc
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Profile.json
@@ -0,0 +1,92 @@
+{
+ "height": 774,
+ "width": 1584,
+ "circles": [
+ {
+ "id": "5b6976c5-c033-4198-a0cd-ba52e6115486",
+ "radius": 202.6121489447264,
+ "x": 612.69,
+ "y": 268.32000000000005
+ },
+ {
+ "id": "ef55cc32-8b9a-4ae1-ae57-256a5d08e847",
+ "radius": 185.88985044913022,
+ "x": 901.65,
+ "y": 531.48
+ },
+ {
+ "id": "9cb95fba-9e6d-4404-8624-d7e86192ae56",
+ "radius": 143.33519665455518,
+ "x": 714.5999999999999,
+ "y": 272.19000000000005
+ },
+ {
+ "id": "ec794f04-f69b-4d6d-a297-0f74d2fb7edc",
+ "radius": 138.45732916678696,
+ "x": 915.84,
+ "y": 583.08
+ },
+ {
+ "id": "652750a2-66e0-43f4-9e7e-14d096fd5e0d",
+ "radius": 131.80745047227035,
+ "x": 859.0799999999999,
+ "y": 322.5
+ },
+ {
+ "id": "a3e8d1e6-dfe2-4694-ac67-2bfc536a567e",
+ "radius": 74.46328759865496,
+ "x": 919.7100000000002,
+ "y": 199.95000000000002
+ },
+ {
+ "id": "2888d28b-b885-49b6-a512-765ffd4aa86b",
+ "radius": 22.081243171524562,
+ "x": 951.9599999999999,
+ "y": 384.42
+ },
+ {
+ "id": "100b74b6-1606-421b-a2a2-ed7b6b3ac687",
+ "radius": 21.93,
+ "x": 922.29,
+ "y": 415.37999999999994
+ },
+ {
+ "id": "bcb98145-0ef4-4c03-801f-0f9cf8d626ce",
+ "radius": 20.191693836823102,
+ "x": 927.45,
+ "y": 313.47
+ },
+ {
+ "id": "32269cf9-1fc1-41c5-901a-f97ec7c8f58b",
+ "radius": 19.392952328101053,
+ "x": 910.68,
+ "y": 341.84999999999997
+ },
+ {
+ "id": "a77ddd12-089c-4ef4-abef-4669636e420a",
+ "radius": 15.53365700664206,
+ "x": 904.2299999999999,
+ "y": 287.67
+ },
+ {
+ "id": "1ea10506-1b14-46a8-8ca3-96af52d27168",
+ "radius": 13.467995396494613,
+ "x": 922.29,
+ "y": 371.52
+ }
+ ],
+ "fills": {
+ "10000000000000000010000000000000100000000000000000000000010000000001010": true,
+ "100000010100000000000010100010000010001000000000000000000000001111000000111100": true,
+ "100000001000000001000100000000000000001010000010000000001000101000000010000100100010000110000101000110000": true,
+ "1000010000000000000000000000000000000000000110000": true,
+ "1000000001000000000000000100000000000000000000000100000001001000000": true,
+ "100010000010000000000000010000000000000000000010000000000100000000001001000001000000000100100000010000": true,
+ "101000010000000000000000001000000000000000000000000000000000001100110000000000000000000000": true,
+ "10100000000100000000001000000000000000000000000000000000000000000000000110000110000000000000000000000000000": true,
+ "1010000001000000000000000000000000000000000000000000001000000000000011000000000000000000110000000000": true,
+ "1000000001010000000000000000000000000000000000000000000000001000000000110000000000000000000000110000000000": true,
+ "10100000000100000000000000001000000000000000000000000000000000000000001100000011000000000000000000000000": true,
+ "100000010000000000000000000000000000000000000000110000000000000000": true
+ }
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg b/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg
new file mode 100644
index 00000000..c1b0e331
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Profile.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Whale.json b/src/components/Projects/CircleArt/Gallery/configurations/Whale.json
new file mode 100644
index 00000000..083559a7
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Whale.json
@@ -0,0 +1,106 @@
+{
+ "height": 774,
+ "width": 1584,
+ "circles": [
+ {
+ "id": "85f64b2b-8189-478d-a418-028a8d972c66",
+ "radius": 576.5362004245701,
+ "x": 812.64,
+ "y": -233.49000000000012
+ },
+ {
+ "id": "a43eaa89-1589-487b-a137-c572187e6719",
+ "radius": 225.16320880641223,
+ "x": 1079.67,
+ "y": 546.96
+ },
+ {
+ "id": "30e60e5a-0c3c-4bff-847a-b43f000728dc",
+ "radius": 223.7062343789283,
+ "x": 838.4399999999999,
+ "y": 261.87
+ },
+ {
+ "id": "d8faef6c-2f68-4e4c-9911-752b859536c1",
+ "radius": 207.4334440248245,
+ "x": 1077.09,
+ "y": 535.35
+ },
+ {
+ "id": "53cca615-adf1-43d1-b9b5-fb0f338aa34b",
+ "radius": 107.93685237211618,
+ "x": 583.02,
+ "y": 263.16
+ },
+ {
+ "id": "058af930-b6ab-4041-babe-578e35135ad8",
+ "radius": 82.73115072329166,
+ "x": 762.3299999999999,
+ "y": 439.89000000000004
+ },
+ {
+ "id": "2dcd8fe8-f859-47d4-85ea-fd2d429fdeb7",
+ "radius": 82.73115072329166,
+ "x": 702.99,
+ "y": 450.21
+ },
+ {
+ "id": "959a5108-68c9-45e8-83ac-2c9ba170c3d4",
+ "radius": 51.35755543247751,
+ "x": 672.03,
+ "y": 294.12
+ },
+ {
+ "id": "ad43a593-e521-4876-8000-5b0de7cd9588",
+ "radius": 51.35755543247751,
+ "x": 598.5,
+ "y": 356.03999999999996
+ },
+ {
+ "id": "49e18223-1742-49b5-9c67-6466f380762f",
+ "radius": 35.93524175513503,
+ "x": 639.78,
+ "y": 215.42999999999998
+ },
+ {
+ "id": "931647b1-ddea-4ad2-aa23-2b95de9ec1d2",
+ "radius": 34.85388070215424,
+ "x": 526.26,
+ "y": 312.18
+ },
+ {
+ "id": "bb1b9b24-2f61-42b7-88e2-a5fe5ceaae84",
+ "radius": 25.442776971077663,
+ "x": 1025.49,
+ "y": 326.37000000000006
+ },
+ {
+ "id": "1379d512-bfce-4745-b282-66f7a136704e",
+ "radius": 5.473006486383879,
+ "x": 896.49,
+ "y": 380.54999999999995
+ }
+ ],
+ "fills": {
+ "1000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001100000000000000000000000000": true,
+ "10100000000000000000000000000000000000000000000000000000000000000000000000010000000001000000000000000000000000000000011001100000000": true,
+ "100000000000010000000000000000101001000000000000010000000000001000100000001000000010000000001000010010000100110000010100100100000": true,
+ "1000000000000000000000000000000000000000000000000010100000000000001000000000000000000000000000000001100000000001100000000000000": true,
+ "10001000000000000000000000000000001000000000000000000000000001000000000000001010000000000000000000": true,
+ "10000100000000000000000000000000010000000000000000000000000001000000000000001001000000000000000000": true,
+ "1000100000000000000000000000000100000000000000000000000000001000000000000000101000000000000000000": true,
+ "10000000000000000000000000000000000100000000000010000000000000000000000000000000000010000000100000100000000000000000": true,
+ "100000000000000000001000000000000000000000000000000100000000000000000010000000000000000000000101000": true,
+ "100000000000101000000000000000000001000000000000000000000000000001000000001100000000010000000000000000000000011000": true,
+ "100000000000010000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000": true,
+ "1010000000000010000000000000000000100000000000100000000000000000100000000001100000010000000000000100000000010000001": true,
+ "1000000000000001000000000000000000010000000000000000000000000000000000000001000000010010000000000000000000000000000": true,
+ "100000000000010100000000000000000000000001000000000000100000000000000001000000011000000000001000000000000100000000001000001": true,
+ "100000000000101000000000000100000000000000000000000000000000000000001000000110000000000100000000000000000000000011000": true,
+ "10001000000000000000000000000001000000000000000000000000010000000000000001010000000000000000000": true,
+ "100000000000010000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000": true,
+ "100000000000001000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000": true,
+ "10000000000000000100000000000000010000000000000000000000000000000000000000000000010000000100100000000000000000000000000000": true,
+ "1000000000000000100000000000000000000000010000000000000000000000000000000000000010000000100001000000000000000000000000000000": true
+ }
+}
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg b/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg
new file mode 100644
index 00000000..b570c5e8
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/Whale.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/Projects/CircleArt/Gallery/configurations/index.ts b/src/components/Projects/CircleArt/Gallery/configurations/index.ts
new file mode 100644
index 00000000..e09afd6a
--- /dev/null
+++ b/src/components/Projects/CircleArt/Gallery/configurations/index.ts
@@ -0,0 +1,35 @@
+import { CircleArtGalleryItem } from "../../types";
+
+const data: CircleArtGalleryItem[] = [{
+ author: 'Dorota Pankoska',
+ authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html',
+ config: require('./Fox.json'),
+ name: 'Fox',
+ thumbnail: require('./Fox.svg'),
+}, {
+ author: 'Dorota Pankoska',
+ authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html',
+ config: require('./Monkey.json'),
+ name: 'Monkey',
+ thumbnail: require('./Monkey.svg'),
+}, {
+ author: 'Dorota Pankoska',
+ authorUrl: 'http://dorotapankowska.com/13-animals-13-circles.html',
+ config: require('./Whale.json'),
+ name: 'Whale',
+ thumbnail: require('./Whale.svg'),
+}, {
+ author: 'Unknown',
+ authorUrl: 'https://www.reddit.com/r/Damnthatsinteresting/comments/963j4n/magic_of_circles',
+ config: require('./Profile.json'),
+ name: 'Profile',
+ thumbnail: require('./Profile.svg'),
+}, {
+ author: 'Me',
+ authorUrl: 'https://hogg.io',
+ config: require('./Mushroom.json'),
+ name: 'Mushroom',
+ thumbnail: require('./Mushroom.svg'),
+}];
+
+export default data;
diff --git a/src/components/Projects/CircleArt/types.ts b/src/components/Projects/CircleArt/types.ts
new file mode 100644
index 00000000..40bfca30
--- /dev/null
+++ b/src/components/Projects/CircleArt/types.ts
@@ -0,0 +1,16 @@
+import { Circle } from "../IntersectionExplorer/useGraph";
+
+export type CircleArtData = {
+ width: number;
+ height: number;
+ circles: Circle[];
+ fills: Record;
+};
+
+export type CircleArtGalleryItem = {
+ author: string;
+ authorUrl: string;
+ config: CircleArtData;
+ name: string;
+ thumbnail: string;
+};
diff --git a/src/components/Projects/CircleArt/utils/cursor.ts b/src/components/Projects/CircleArt/utils/cursor.ts
new file mode 100644
index 00000000..4bded9d5
--- /dev/null
+++ b/src/components/Projects/CircleArt/utils/cursor.ts
@@ -0,0 +1,40 @@
+import atan2 from "./math/atan2";
+
+type CircleEditorCursor =
+ | 'default'
+ | 'crosshair'
+ | 'pointer'
+ | 'move'
+ | 'ns-resize'
+ | 'nesw-resize'
+ | 'ew-resize'
+ | 'nwse-resize';
+
+
+export const CURSOR_DEFAULT: CircleEditorCursor = 'default';
+export const CURSOR_DRAW: CircleEditorCursor = 'crosshair';
+export const CURSOR_FILL: CircleEditorCursor = 'pointer';
+export const CURSOR_MOVE: CircleEditorCursor = 'move';
+export const CURSOR_RESIZE_T_B: CircleEditorCursor = 'ns-resize';
+export const CURSOR_RESIZE_BL_TR: CircleEditorCursor = 'nesw-resize';
+export const CURSOR_RESIZE_L_R: CircleEditorCursor = 'ew-resize';
+export const CURSOR_RESIZE_BR_TL: CircleEditorCursor = 'nwse-resize';
+
+export const getCursor = (px: number, py: number, cx: number, cy: number) => {
+ const a = (atan2(px, py, cx, cy) * 180) / Math.PI;
+
+ if (a > 247.5 && a < 292.5) return CURSOR_RESIZE_T_B;
+ if (a > 292.5 && a < 337.5) return CURSOR_RESIZE_BL_TR;
+ if (a > 337.5 || a < 22.5) return CURSOR_RESIZE_L_R;
+ if (a > 22.5 && a < 67.5) return CURSOR_RESIZE_BR_TL;
+ if (a > 67.5 && a < 112.5) return CURSOR_RESIZE_T_B;
+ if (a > 112.5 && a < 157.5) return CURSOR_RESIZE_BL_TR;
+ if (a > 157.5 && a < 202.5) return CURSOR_RESIZE_L_R;
+ if (a > 202.5 && a < 247.5) return CURSOR_RESIZE_BR_TL;
+
+ return CURSOR_DRAW;
+};
+
+export const setCursor = (element: null | HTMLElement | SVGElement, cursor: CircleEditorCursor) => {
+ if (element) element.style.cursor = cursor;
+};
diff --git a/src/components/Projects/CircleArt/utils/math/atan2.ts b/src/components/Projects/CircleArt/utils/math/atan2.ts
new file mode 100644
index 00000000..2085c4c3
--- /dev/null
+++ b/src/components/Projects/CircleArt/utils/math/atan2.ts
@@ -0,0 +1,5 @@
+export default (x1: number, y1: number, x2: number, y2: number, normalise = true) => {
+ let a = Math.atan2(y1 - y2, x1 - x2);
+ if (normalise && a < 0) a += Math.PI * 2;
+ return a;
+};
diff --git a/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts b/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts
new file mode 100644
index 00000000..e85d21c1
--- /dev/null
+++ b/src/components/Projects/CircleArt/utils/math/isPointOverCircleEdge.ts
@@ -0,0 +1,5 @@
+import isPointWithinCircle from './isPointWithinCircle';
+
+export default (px: number, py: number, cx: number, cy: number, radius: number, padding: number = 0) =>
+ isPointWithinCircle(px, py, cx, cy, radius, padding) &&
+ !isPointWithinCircle(px, py, cx, cy, radius, padding * -1);
diff --git a/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts b/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts
new file mode 100644
index 00000000..8dfb26b1
--- /dev/null
+++ b/src/components/Projects/CircleArt/utils/math/isPointWithinCircle.ts
@@ -0,0 +1,3 @@
+export default (px: number, py: number, cx: number, cy: number, radius: number, padding: number = 0) =>
+ ((px - cx) ** 2) + ((py - cy) ** 2) < ((radius + padding) ** 2);
+
diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx b/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx
index bcaac434..24d8a754 100644
--- a/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx
+++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/GraphVisualisation.tsx
@@ -3,107 +3,38 @@ import { motion } from 'framer-motion';
import { Box, useResizeObserver } from 'preshape';
import React, { useContext, useMemo, useRef, PointerEvent } from 'react';
import { IntersectionExplorerContext } from '../IntersectionExplorer';
-import { Edge, Node, Traversal } from '../useGraph';
import GraphVisualisationEdge from './GraphVisualisationEdge';
import GraphVisualisationLabel from './GraphVisualisationLabel';
import GraphVisualisationNode from './GraphVisualisationNode';
import GraphVisualisationTraversal from './GraphVisualisationTraversal';
+import getArcPath from './getArcPath';
+import getScaledProps from './getScaledProps';
+import getTraversalPath from './getTraversalPath';
import useLabelPositionShifts from './useLabelPositionShifts';
import './GraphVisualisation.css';
-const scale = (v: number, m: number) => m * (v / 1);
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-const scaleProps = (
- entities: T[],
- props: (keyof T)[],
- range: number
-): T[] => {
- return entities.map((entity) => {
- const entityScaled: T = { ...entity };
-
- for (const prop of props) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- entityScaled[prop] = scale(
- entityScaled[prop] as unknown as number,
- range
- ) as any;
- }
-
- return entityScaled;
- });
-};
-
-const getArcPath = (
- edge: Edge,
- nodes: Node[],
- start = true,
- reverse = false
-): string => {
- const {
- angleStart,
- angleEnd,
- nodes: [a, b],
- radius,
- } = edge;
- const { x: sx, y: sy } = reverse ? nodes[b] : nodes[a];
- const { x: ex, y: ey } = reverse ? nodes[a] : nodes[b];
-
- const largeArcFlag = Math.abs(angleEnd - angleStart) >= Math.PI ? 1 : 0;
- const sweepFlag = reverse ? 0 : 1;
-
- return (
- (start ? `M ${sx} ${sy} ` : '') +
- `A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${ex} ${ey} `
- );
-};
-
-const getTraversalPath = (
- traversal: Traversal,
- nodes: Node[],
- edges: Edge[]
-): string => {
- let path = '';
-
- for (let i = 1; i < traversal.path.length; i += 2) {
- const e = traversal.path[i] - nodes.length;
- const a = traversal.path[i - 1];
- const reverse = edges[e].nodes[0] !== a;
-
- path +=
- getArcPath(
- edges[traversal.path[i] - nodes.length],
- nodes,
- i === 1,
- reverse
- ) + ' ';
- }
-
- return path;
-};
-
interface Props {
onNodeOver: (index: number) => void;
onTraversalOver: (index: number) => void;
}
const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => {
- const { activeNodeIndex, addToTraversal, graph, traversals } = useContext(
+ const { activeNodeIndex, addToTraversal, graph } = useContext(
IntersectionExplorerContext
);
const [size, ref] = useResizeObserver();
const min = size.width;
const circles = useMemo(
- () => scaleProps(graph.circles, ['radius', 'x', 'y'], min),
+ () => getScaledProps(graph.circles, ['radius', 'x', 'y'], min),
[min]
);
const edges = useMemo(
- () => scaleProps(graph.edges, ['radius', 'x', 'y'], min),
+ () => getScaledProps(graph.edges, ['radius', 'x', 'y'], min),
[graph, min]
);
const nodes = useMemo(
- () => scaleProps(graph.nodes, ['x', 'y'], min),
+ () => getScaledProps(graph.nodes, ['x', 'y'], min),
[graph, min]
);
const classes = classNames('GraphVisualisation', {
@@ -113,7 +44,7 @@ const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => {
const refLabels = useRef(null);
const refObstacles = useRef(null);
const labelPositionShifts = useLabelPositionShifts(
- { circles, edges, nodes },
+ { ...graph, circles, edges, nodes },
refLabels.current,
refObstacles.current
);
@@ -155,7 +86,7 @@ const GraphVisualisation = ({ onNodeOver, onTraversalOver }: Props) => {
))}
{/* Animating yellow traversals */}
- {traversals.map((traversal, index) => (
+ {graph.traversals.map((traversal, index) => (
{
const { d, index, onPointerOver, traversal } = props;
- const { activeNodeIndex, activeTraversalIndex, traversals } = useContext(
+ const { activeNodeIndex, activeTraversalIndex, graph } = useContext(
IntersectionExplorerContext
);
- const currentTraversal = getCurrentTraversal(traversals);
+ const currentTraversal = getCurrentTraversal(graph.traversals);
const refGlow = useRef(null);
const refPath = useRef(null);
diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts
new file mode 100644
index 00000000..f6fa2ea5
--- /dev/null
+++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getArcPath.ts
@@ -0,0 +1,27 @@
+import { Edge, Node } from "../useGraph";
+
+const getArcPath = (
+ edge: Edge,
+ nodes: Node[],
+ start = true,
+ reverse = false
+): string => {
+ const {
+ angleStart,
+ angleEnd,
+ nodes: [a, b],
+ radius,
+ } = edge;
+ const { x: sx, y: sy } = reverse ? nodes[b] : nodes[a];
+ const { x: ex, y: ey } = reverse ? nodes[a] : nodes[b];
+
+ const largeArcFlag = Math.abs(angleEnd - angleStart) >= Math.PI ? 1 : 0;
+ const sweepFlag = reverse ? 0 : 1;
+
+ return (
+ (start ? `M ${sx} ${sy} ` : '') +
+ `A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${ex} ${ey} `
+ );
+};
+
+export default getArcPath;
diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts
new file mode 100644
index 00000000..5cd77e0f
--- /dev/null
+++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getScaledProps.ts
@@ -0,0 +1,22 @@
+export const scale = (v: number, m: number) => m * (v / 1);
+
+const getScaledProps = (
+ entities: T[],
+ props: (keyof T)[],
+ range: number
+): T[] => {
+ return entities.map((entity) => {
+ const entityScaled: T = { ...entity };
+
+ for (const prop of props) {
+ entityScaled[prop] = scale(
+ entityScaled[prop] as unknown as number,
+ range
+ ) as any;
+ }
+
+ return entityScaled;
+ });
+};
+
+export default getScaledProps;
diff --git a/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts
new file mode 100644
index 00000000..4987819f
--- /dev/null
+++ b/src/components/Projects/IntersectionExplorer/GraphVisualisation/getTraversalPath.ts
@@ -0,0 +1,28 @@
+import { Traversal, Edge, Node } from "../useGraph";
+import getArcPath from "./getArcPath";
+
+const getTraversalPath = (
+ traversal: Traversal,
+ nodes: Node[],
+ edges: Edge[]
+): string => {
+ let path = '';
+
+ for (let i = 1; i < traversal.path.length; i += 2) {
+ const e = traversal.path[i] - nodes.length;
+ const a = traversal.path[i - 1];
+ const reverse = edges[e].nodes[0] !== a;
+
+ path +=
+ getArcPath(
+ edges[traversal.path[i] - nodes.length],
+ nodes,
+ i === 1,
+ reverse
+ ) + ' ';
+ }
+
+ return path;
+};
+
+export default getTraversalPath;
diff --git a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css
index 18d861e1..9535cc08 100644
--- a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css
+++ b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.css
@@ -11,11 +11,12 @@
}
.IntersectionExplorer--120 {
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 300px 1fr;
+ grid-template-rows: min-content 1fr;
& > :nth-child(1) { grid-column: 1; grid-row: 1; }
- & > :nth-child(2) { grid-column: 2; grid-row: 1; }
- & > :nth-child(3) { grid-column: span 2; grid-row: 2; }
+ & > :nth-child(2) { grid-column: 2; grid-row: span 2; }
+ & > :nth-child(3) { grid-column: 1; grid-row: 2; }
}
.IntersectionExplorer--111 {
diff --git a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx
index 809ebbea..38556d46 100644
--- a/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx
+++ b/src/components/Projects/IntersectionExplorer/IntersectionExplorer.tsx
@@ -1,5 +1,5 @@
import classnames from 'classnames';
-import { Box, useMatchMedia } from 'preshape';
+import { Box, Text, useMatchMedia } from 'preshape';
import React, { createContext, useEffect, useRef, useState } from 'react';
import GraphVisualisation from './GraphVisualisation/GraphVisualisation';
import NodeList from './NodeList/NodeList';
@@ -36,8 +36,8 @@ export const IntersectionExplorerContext = createContext({
circles: [],
edges: [],
nodes: [],
+ traversals: [],
},
- traversals: [],
});
const IntersectionExplorer = ({
@@ -105,6 +105,10 @@ const IntersectionExplorer = ({
ref={refContainer}
>
+
+ Nodes & Edges
+
+
setActiveNodeIndex(i)} />
@@ -116,6 +120,10 @@ const IntersectionExplorer = ({
+
+ Traversals
+
+
setActiveTraversalIndex(i)}
/>
diff --git a/src/components/Projects/IntersectionExplorer/NodeList/NodeList.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"