diff --git a/.DS_Store b/.DS_Store index 8a67fffd..443b3513 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/package.json b/package.json index 183ed34a..0d9a1da7 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", + "react-router-dom": "^6.21.1", "react-transition-group": "^4.4.5", "rebound": "^0.1.0", "regenerator-runtime": "^0.14.0", diff --git a/src/infinite-scroll/CaterpillarScroller.tsx b/src/infinite-scroll/CaterpillarScroller.tsx index 62435f8e..f1e22c4d 100644 --- a/src/infinite-scroll/CaterpillarScroller.tsx +++ b/src/infinite-scroll/CaterpillarScroller.tsx @@ -317,8 +317,8 @@ function WormyScrollbar({ 3.5, 2, 0, - 0, - 0, + false, + false, lineBasePx + 1, effectiveTopPx + effectiveHeightPx + 2, ) diff --git a/src/lib/DebugSvg.tsx b/src/lib/DebugSvg.tsx index 75a0d458..a8a99919 100644 --- a/src/lib/DebugSvg.tsx +++ b/src/lib/DebugSvg.tsx @@ -1,3 +1,4 @@ +import { scale } from "@/blob-tree/canvas"; import { DebugDraw, FillOptions, @@ -5,6 +6,7 @@ import { StrokeOptions, } from "@/lib/DebugDraw"; import { Vector2, Vector2Ish } from "@/lib/geom/Vector2"; +import { useSvgScale } from "@/lib/react/Svg"; import { SvgPathBuilder } from "@/lib/svgPathBuilder"; import { ComponentProps } from "react"; @@ -12,7 +14,7 @@ function asProps() { return >(props: T) => props; } -function getStrokeProps({ +function useStrokeProps({ strokeWidth = 1, stroke = "transparent", strokeCap = "butt", @@ -20,13 +22,14 @@ function getStrokeProps({ strokeDashOffset = 0, strokeJoin = "round", }: StrokeOptions = {}) { + const scale = useSvgScale(); return asProps<"path">()({ stroke: stroke, - strokeWidth, + strokeWidth: strokeWidth / scale, strokeLinecap: strokeCap, strokeLinejoin: strokeJoin, - strokeDasharray: strokeDash.join(" "), - strokeDashoffset: strokeDashOffset, + strokeDasharray: strokeDash.map((dash) => dash / scale).join(" "), + strokeDashoffset: strokeDashOffset / scale, }); } @@ -36,9 +39,9 @@ function getFillProps({ fill = "transparent" }: FillOptions = {}) { }); } -function getStrokeAndFillProps(options: StrokeAndFillOptions) { +function useStrokeAndFillProps(options: StrokeAndFillOptions) { return { - ...getStrokeProps(options), + ...useStrokeProps(options), ...getFillProps(options), }; } @@ -58,16 +61,19 @@ export function DebugLabel({ position: Vector2Ish; color?: string; }) { + const scale = useSvgScale(); if (!label) return null; - const adjustedPosition = Vector2.from(position).add(DebugDraw.LABEL_OFFSET); + const adjustedPosition = Vector2.from(position).add( + DebugDraw.LABEL_OFFSET.scale(1 / scale), + ); return ( {label} @@ -75,7 +81,10 @@ export function DebugLabel({ ); } -interface DebugOptions { color?: string; label?: string } +interface DebugOptions { + color?: string; + label?: string; +} export function DebugSvgPath({ color, @@ -87,7 +96,7 @@ export function DebugSvgPath({ return ( ); @@ -97,6 +106,7 @@ export function DebugPointX({ position, ...debugOpts }: { position: Vector2Ish } & DebugOptions) { + const scale = useSvgScale(); const { x, y } = Vector2.from(position); return ( <> @@ -104,20 +114,20 @@ export function DebugPointX({ {...debugOpts} path={new SvgPathBuilder() .moveTo( - x - DebugDraw.DEBUG_POINT_SIZE, - y - DebugDraw.DEBUG_POINT_SIZE, + x - DebugDraw.DEBUG_POINT_SIZE / scale, + y - DebugDraw.DEBUG_POINT_SIZE / scale, ) .lineTo( - x + DebugDraw.DEBUG_POINT_SIZE, - y + DebugDraw.DEBUG_POINT_SIZE, + x + DebugDraw.DEBUG_POINT_SIZE / scale, + y + DebugDraw.DEBUG_POINT_SIZE / scale, ) .moveTo( - x - DebugDraw.DEBUG_POINT_SIZE, - y + DebugDraw.DEBUG_POINT_SIZE, + x - DebugDraw.DEBUG_POINT_SIZE / scale, + y + DebugDraw.DEBUG_POINT_SIZE / scale, ) .lineTo( - x + DebugDraw.DEBUG_POINT_SIZE, - y - DebugDraw.DEBUG_POINT_SIZE, + x + DebugDraw.DEBUG_POINT_SIZE / scale, + y - DebugDraw.DEBUG_POINT_SIZE / scale, ) .toString()} /> @@ -130,14 +140,15 @@ export function DebugPointO({ position, ...debugOpts }: { position: Vector2Ish } & DebugOptions) { + const scale = useSvgScale(); const { x, y } = Vector2.from(position); return ( <> @@ -149,17 +160,18 @@ export function DebugArrow({ end: _end, ...debugOpts }: { start: Vector2Ish; end: Vector2Ish } & DebugOptions) { + const scale = useSvgScale(); const start = Vector2.from(_start); const end = Vector2.from(_end); const vector = end.sub(start); const arrowLeftPoint = vector .rotate(-DebugDraw.DEBUG_ARROW_ANGLE) - .withMagnitude(DebugDraw.DEBUG_ARROW_SIZE) + .withMagnitude(DebugDraw.DEBUG_ARROW_SIZE / scale) .add(end); const arrowRightPoint = vector .rotate(+DebugDraw.DEBUG_ARROW_ANGLE) - .withMagnitude(DebugDraw.DEBUG_ARROW_SIZE) + .withMagnitude(DebugDraw.DEBUG_ARROW_SIZE / scale) .add(end); return ( @@ -208,7 +220,7 @@ export function DebugCircle({ cx={x} cy={y} r={radius} - {...getStrokeAndFillProps( + {...useStrokeAndFillProps( getDebugStrokeOptions(debugOpts.color), )} /> diff --git a/src/lib/geom/AABB.ts b/src/lib/geom/AABB.ts index 9d1fddbe..e8bb1496 100644 --- a/src/lib/geom/AABB.ts +++ b/src/lib/geom/AABB.ts @@ -1,4 +1,5 @@ import { Vector2 } from "@/lib/geom/Vector2"; +import { mapRange } from "@/lib/utils"; export default class AABB { static fromLeftTopRightBottom( @@ -22,6 +23,20 @@ export default class AABB { return new AABB(new Vector2(left, top), new Vector2(width, height)); } + static from({ + x, + y, + width, + height, + }: { + x: number; + y: number; + width: number; + height: number; + }) { + return AABB.fromLeftTopWidthHeight(x, y, width, height); + } + constructor( public readonly origin: Vector2, public readonly size: Vector2, diff --git a/src/lib/geom/CirclePathSegment.ts b/src/lib/geom/CirclePathSegment.ts index d0a84dd5..8f0b2fbc 100644 --- a/src/lib/geom/CirclePathSegment.ts +++ b/src/lib/geom/CirclePathSegment.ts @@ -89,8 +89,8 @@ export default class CirclePathSegment implements PathSegment { this.circle.radius, this.circle.radius, 0, - 0, - this.isAnticlockwise ? 0 : 1, + false, + !this.isAnticlockwise, this.getEnd(), ); } diff --git a/src/lib/geom/Vector2.ts b/src/lib/geom/Vector2.ts index 1fe56cbe..a422c65f 100644 --- a/src/lib/geom/Vector2.ts +++ b/src/lib/geom/Vector2.ts @@ -1,7 +1,13 @@ import { Result } from "@/lib/Result"; import { assert } from "@/lib/assert"; +import AABB from "@/lib/geom/AABB"; import { Schema } from "@/lib/schema"; -import { lerp, normalizeAngle } from "@/lib/utils"; +import { + constrain as clamp, + lerp, + mapRange, + normalizeAngle, +} from "@/lib/utils"; export type Vector2Ish = | { readonly x: number; readonly y: number } @@ -161,8 +167,9 @@ export class Vector2 { scale(scale: number): Vector2 { return new Vector2(this.x * scale, this.y * scale); } - mul(scale: number): Vector2 { - return this.scale(scale); + mul(...args: Vector2Args): Vector2 { + const other = Vector2.fromArgs(args); + return new Vector2(this.x * other.x, this.y * other.y); } negate(): Vector2 { @@ -234,4 +241,18 @@ export class Vector2 { project(direction: Vector2Ish, distance: number): Vector2 { return Vector2.from(direction).scale(distance).add(this); } + + mapRange(from: AABB, to: AABB) { + return new Vector2( + mapRange(from.left, from.right, to.left, to.right, this.x), + mapRange(from.top, from.bottom, to.top, to.bottom, this.y), + ); + } + + clamp(within: AABB) { + return new Vector2( + clamp(within.left, within.right, this.x), + clamp(within.top, within.bottom, this.y), + ); + } } diff --git a/src/lib/react/Svg.tsx b/src/lib/react/Svg.tsx new file mode 100644 index 00000000..04514098 --- /dev/null +++ b/src/lib/react/Svg.tsx @@ -0,0 +1,105 @@ +import { assertExists } from "@/lib/assert"; +import AABB from "@/lib/geom/AABB"; +import { Vector2 } from "@/lib/geom/Vector2"; +import { useEvent } from "@/lib/hooks/useEvent"; +import { useMergedRefs } from "@/lib/hooks/useMergedRefs"; +import { + sizeFromContentRect, + useResizeObserver, +} from "@/lib/hooks/useResizeObserver"; +import { + ComponentProps, + ForwardedRef, + createContext, + forwardRef, + useContext, + useEffect, + useRef, + useState, +} from "react"; + +const SvgScaleContext = createContext(1); +export function useSvgScale() { + return useContext(SvgScaleContext); +} + +export const Svg = forwardRef(function Svg( + { + viewBox, + ...props + }: Omit, "viewBox"> & { viewBox: AABB }, + ref: ForwardedRef, +) { + const [svg, setSvg] = useState(null); + const size = useResizeObserver(svg, sizeFromContentRect); + + const scale = size ? size.x / viewBox.width : 1; + + return ( + + + + ); +}); + +export function SvgApp({ + viewBox, + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel, + ...props +}: Omit< + ComponentProps, + "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel" +> & { + onPointerDown?: (point: Vector2, e: PointerEvent) => void; + onPointerMove?: (point: Vector2, e: PointerEvent) => void; + onPointerUp?: (point: Vector2, e: PointerEvent) => void; + onPointerCancel?: (point: Vector2, e: PointerEvent) => void; +}) { + const svgRef = useRef(null); + function getPointInSvgSpace(e: PointerEvent) { + const svgRect = AABB.from( + assertExists(svgRef.current).getBoundingClientRect(), + ); + return Vector2.fromEvent(e).mapRange(svgRect, viewBox); + } + + const handlePointerDown = useEvent((e: PointerEvent) => { + onPointerDown?.(getPointInSvgSpace(e), e); + }); + const handlePointerMove = useEvent((e: PointerEvent) => { + onPointerMove?.(getPointInSvgSpace(e), e); + }); + const handlePointerUp = useEvent((e: PointerEvent) => { + onPointerUp?.(getPointInSvgSpace(e), e); + }); + const handlePointerCancel = useEvent((e: PointerEvent) => { + onPointerCancel?.(getPointInSvgSpace(e), e); + }); + + useEffect(() => { + window.addEventListener("pointerdown", handlePointerDown); + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp); + window.addEventListener("pointercancel", handlePointerCancel); + return () => { + window.removeEventListener("pointerdown", handlePointerDown); + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + window.removeEventListener("pointercancel", handlePointerCancel); + }; + }, [ + handlePointerCancel, + handlePointerDown, + handlePointerMove, + handlePointerUp, + ]); + + return ; +} diff --git a/src/lib/svgPathBuilder.tsx b/src/lib/svgPathBuilder.tsx index 74d41528..3aa190c8 100644 --- a/src/lib/svgPathBuilder.tsx +++ b/src/lib/svgPathBuilder.tsx @@ -67,14 +67,16 @@ export class SvgPathBuilder { rx: number, ry: number, xAxisRotation: number, - largeArcFlag: number, - sweepFlag: number, + largeArcFlag: boolean, + sweepFlag: boolean, ...args: Vector2Args ) { const position = Vector2.fromArgs(args); this.lastPoint = position; return this.add( - `A${rx} ${ry} ${xAxisRotation} ${largeArcFlag} ${sweepFlag} ${position.x} ${position.y}`, + `A${rx} ${ry} ${xAxisRotation} ${largeArcFlag ? 1 : 0} ${ + sweepFlag ? 1 : 0 + } ${position.x} ${position.y}`, ); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c12618c9..c52a05f7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -204,9 +204,7 @@ export function keys( return Object.keys(object) as K[]; } -export function values( - object: ReadonlyRecord, -): V[] { +export function values(object: ReadonlyRecord): V[] { return Object.values(object); } @@ -382,10 +380,7 @@ export function copyArrayAndReplace( return copied; } -export function copyAndRemove( - array: readonly T[], - index: number, -): T[] { +export function copyAndRemove(array: readonly T[], index: number): T[] { const copied = array.slice(); copied.splice(index, 1); return copied; @@ -466,10 +461,7 @@ export function radiansToDegrees(radians: number) { return (radians * 180) / Math.PI; } -export function windows( - array: readonly T[], - size: number, -): T[][] { +export function windows(array: readonly T[], size: number): T[][] { const result = []; for (let i = 0; i < array.length - size + 1; i++) { result.push(array.slice(i, i + size)); diff --git a/src/lime/Viewport.tsx b/src/lime/Viewport.tsx index 50f3e51d..830671c9 100644 --- a/src/lime/Viewport.tsx +++ b/src/lime/Viewport.tsx @@ -18,7 +18,7 @@ export class Viewport { } @memo get availableCanvasSize() { - return this.canvasSize.sub(LIME_SLIDE_PADDING_PX.mul(2)); + return this.canvasSize.sub(LIME_SLIDE_PADDING_PX.scale(2)); } @memo get scaleFactor() { @@ -54,7 +54,7 @@ export class Viewport { slideToScreen(slideCoords: Vector2) { return slideCoords - .mul(this.scaleFactor) + .scale(this.scaleFactor) .add(this.slideOffset) .add(this.canvasOffset); } diff --git a/src/spline-time/layers/arcLayer.tsx b/src/spline-time/layers/arcLayer.tsx index 7ddcc401..f1710a0b 100644 --- a/src/spline-time/layers/arcLayer.tsx +++ b/src/spline-time/layers/arcLayer.tsx @@ -73,8 +73,8 @@ export function arcLayer({ line, uiTarget, showExtras }: LayerProps) { pathSegment.circle.radius, pathSegment.circle.radius, 0, - 0, - pathSegment.isAnticlockwise ? 0 : 1, + false, + !pathSegment.isAnticlockwise, arcEndPoint, ); } else { diff --git a/src/trees/LeafPattern.tsx b/src/trees/LeafPattern.tsx new file mode 100644 index 00000000..f36d6454 --- /dev/null +++ b/src/trees/LeafPattern.tsx @@ -0,0 +1,338 @@ +import { DebugArrow, DebugPointX } from "@/lib/DebugSvg"; +import AABB from "@/lib/geom/AABB"; +import { Vector2 } from "@/lib/geom/Vector2"; +import { SvgApp } from "@/lib/react/Svg"; +import { clamp, mapRange } from "@/lib/utils"; +import { useEffect, useState } from "react"; +import * as easings from "@/lib/easings"; +import { assert } from "@/lib/assert"; +import { SvgPathBuilder } from "@/lib/svgPathBuilder"; +import { degToRad } from "three/src/math/MathUtils"; +import { useNoise4d } from "@/trees/TreesApp"; +import { Ticker } from "@/lib/Ticker"; +import { track } from "@tldraw/state"; + +const viewbox = AABB.fromLeftTopWidthHeight(0, 0, 100, 100); +const unitRange = AABB.fromLeftTopRightBottom(-1, -1, 1, 1); + +const flavours = [ + new Vector2(1, 1), + new Vector2(1, -1), + new Vector2(-1, -1), + new Vector2(-1, 1), +]; + +const triHeight = Math.sqrt(3) / 2; +const xStep = 10; +const yStep = xStep * triHeight; +const spacing = new Vector2(xStep, yStep); + +export const LeafPatternApp = track(function LeafPatternApp() { + const [xy, setXy] = useState(Vector2.ZERO); + const relative = xy.mapRange(viewbox, unitRange).clamp(unitRange); + const [t] = useState(() => new Ticker()); + + useEffect(() => { + t.start(); + return () => { + t.stop(); + }; + }, [t]); + + const noiseX = useNoise4d(xy, t, { scaleT: 10 }); + const noiseY = useNoise4d(xy, t, { scaleT: 10 }); + + const leafs = []; + for (let iY = 0; iY < viewbox.height / yStep; iY++) { + for (let iX = 0; iX < viewbox.width / xStep; iX++) { + const relative = new Vector2(noiseX(iX / 8), noiseY(iY / 8)) + .scale(3) + .clamp(unitRange); + leafs.push( + , + ); + } + } + + return ( +
+ setXy(p)} + > + + {/* {normalizedWeights.map((w, i) => { + const start = new Vector2(10, 10 + i * 10); + + return ( + + ); + })} */} + {leafs} + +
+ ); +}); + +interface LeafStructure { + lines: [LeafLine, LeafLine, LeafLine, LeafLine]; + weight: number; + origin: (idx: Vector2, spacing: Vector2) => Vector2; +} +interface LeafLine { + start: Vector2; + end: Vector2; + c1: Vector2; + c2: Vector2; +} + +export function Leaf({ + weights, + size, + strokeWeight, + originIdx, +}: { + weights: number[]; + size: number; + strokeWeight: number; + originIdx: Vector2; + spacing: Vector2; +}) { + assert(weights.length === structureFlavours.length); + + const compositeFlavour = { + weight: 0, + origin: new Vector2(0, 0), + lines: [ + straight(0, 0, 0, 0), + straight(0, 0, 0, 0), + straight(0, 0, 0, 0), + straight(0, 0, 0, 0), + ], + }; + + for (let i = 0; i < weights.length; i++) { + const flavour = structureFlavours[i]; + const weight = weights[i]; + + compositeFlavour.weight += weight * flavour.weight * strokeWeight; + compositeFlavour.origin = compositeFlavour.origin.add( + flavour.origin(originIdx, spacing).scale(weight), + ); + compositeFlavour.lines.forEach((line, j) => { + addLineInPlace( + line, + flavour.lines[j as any], + weight * flavour.weight * size, + ); + }); + } + + console.log(compositeFlavour); + + const path = new SvgPathBuilder(); + // .moveTo(compositeFlavour.lines[0].start) + // .bezierCurveTo( + // compositeFlavour.lines[0].c1, + // compositeFlavour.lines[0].c2, + // compositeFlavour.lines[0].end, + // ) + // .moveTo(compositeFlavour.lines[1].start) + // .bezierCurveTo( + // compositeFlavour.lines[1].c1, + // compositeFlavour.lines[1].c2, + // compositeFlavour.lines[1].end, + // ); + + compositeFlavour.lines.forEach((line) => { + path.moveTo(compositeFlavour.origin.add(line.start)); + path.bezierCurveTo( + compositeFlavour.origin.add(line.c1), + compositeFlavour.origin.add(line.c2), + compositeFlavour.origin.add(line.end), + ); + }); + + return ( + + ); +} + +function getWeightsFromPoint(relative: Vector2) { + const rawFlavourWeights = flavours.map((f) => + easings.inSin( + clamp(0, 1, mapRange(0, 2, 1, 0, f.distanceTo(relative))), + ), + ); + const totalDistance = rawFlavourWeights.reduce((a, b) => a + b, 0); + const normalizedWeights = rawFlavourWeights.map((w) => w / totalDistance); + return normalizedWeights; +} + +const grid = (idx: Vector2, spacing: Vector2) => { + return idx.mul(spacing); +}; +const staggered = (idx: Vector2, spacing: Vector2) => { + const offsetX = idx.y % 2 === 0 ? -spacing.x / 4 : spacing.x / 4; + return idx.mul(spacing).add(offsetX, 0); +}; + +const leafStrutures = { + dashes: () => { + return { + weight: 1, + origin: staggered, + lines: [ + straight(0, -0.5, 0, 0), + straight(0, 0, 0, 0.5), + straight(0, -0.5, 0, 0), + straight(0, 0, 0, 0.5), + ], + }; + }, + separatedPeaks: () => { + const offset = 0.2; + return { + weight: 1, + origin: grid, + lines: [ + straight(-offset, -(0.5 - offset), -0.5, 0), + straight(-0.5, 0, -(1 - offset), 0.5 - offset), + straight(offset, -(0.5 - offset), 0.5, 0), + straight(0.5, 0, 1 - offset, 0.5 - offset), + ], + }; + }, + ohs: () => { + return { + weight: 1, + origin: staggered, + lines: [ + arc(0, -0.5, -0.5, 0, 0, 0), + arc(-0.5, 0, 0, 0.5, 0, 0), + arc(0, -0.5, 0.5, 0, 0, 0), + arc(0.5, 0, 0, 0.5, 0, 0), + ], + }; + }, + yous: () => { + const origin = new Vector2(0, -0.25); + const arcStart = origin.add(0, 0.5); + const arcMidPoint = arcStart.rotateAround(origin, degToRad(45)); + const arcEnd = arcStart.rotateAround(origin, degToRad(90)); + + return { + weight: 1, + origin: staggered, + lines: [ + arc( + arcStart.x, + arcStart.y, + arcMidPoint.x, + arcMidPoint.y, + origin.x, + origin.y, + ), + arc( + arcMidPoint.x, + arcMidPoint.y, + arcEnd.x, + arcEnd.y, + origin.x, + origin.y, + ), + arc( + -arcStart.x, + arcStart.y, + -arcMidPoint.x, + arcMidPoint.y, + origin.x, + origin.y, + ), + arc( + -arcMidPoint.x, + arcMidPoint.y, + -arcEnd.x, + arcEnd.y, + origin.x, + origin.y, + ), + ], + }; + }, +} satisfies Record LeafStructure>; + +const structureFlavours = [ + leafStrutures.yous(), + leafStrutures.ohs(), + leafStrutures.dashes(), + leafStrutures.separatedPeaks(), +]; + +function straight(x1: number, y1: number, x2: number, y2: number): LeafLine { + const start = new Vector2(x1, y1); + const end = new Vector2(x2, y2); + const control = start.lerp(end, 0.5); + return { + start, + end, + c1: control, + c2: control, + }; +} + +function arc( + xFrom: number, + yFrom: number, + xTo: number, + yTo: number, + xCenter: number, + yCenter: number, +): LeafLine { + const ax = xFrom - xCenter; + const ay = yFrom - yCenter; + const bx = xTo - xCenter; + const by = yTo - yCenter; + const q1 = ax * ax + ay * ay; + const q2 = q1 + ax * bx + ay * by; + const k2 = ((4 / 3) * (Math.sqrt(2 * q1 * q2) - q2)) / (ax * by - ay * bx); + + const cx1 = xCenter + ax - k2 * ay; + const cy1 = yCenter + ay + k2 * ax; + const cx2 = xCenter + bx + k2 * by; + const cy2 = yCenter + by - k2 * bx; + + return { + start: new Vector2(xFrom, yFrom), + end: new Vector2(xTo, yTo), + c1: new Vector2(cx1, cy1), + c2: new Vector2(cx2, cy2), + }; +} + +function addLineInPlace(base: LeafLine, line: LeafLine, weight: number) { + base.start = base.start.add(line.start.scale(weight)); + base.end = base.end.add(line.end.scale(weight)); + base.c1 = base.c1.add(line.c1.scale(weight)); + base.c2 = base.c2.add(line.c2.scale(weight)); +} diff --git a/src/trees/TreesApp.tsx b/src/trees/TreesApp.tsx index fd4c9c7e..e520c705 100644 --- a/src/trees/TreesApp.tsx +++ b/src/trees/TreesApp.tsx @@ -1,21 +1,20 @@ -import { DebugLabel } from "@/lib/DebugSvg"; import { Spring } from "@/lib/Spring"; import { Ticker } from "@/lib/Ticker"; import { Vector2 } from "@/lib/geom/Vector2"; import { SvgPathBuilder } from "@/lib/svgPathBuilder"; -import { constrain, lerp, mapRange, noop, times } from "@/lib/utils"; +import { constrain, invLerp, mapRange, times } from "@/lib/utils"; import { track } from "@tldraw/state"; import { makeNoise3D, makeNoise4D } from "open-simplex-noise"; -import { useEffect, useRef, useState } from "react"; -import { x } from "vitest/dist/reporters-OH1c16Kq"; +import { useEffect, useState } from "react"; +import { degToRad } from "three/src/math/MathUtils"; const SCALE_T = 0.0001; -const SCALE_XY = 0.003; +const SCALE_XY = 0.001; -type NoiseOpts = { +interface NoiseOpts { scaleT?: number; scaleXy?: number; -}; +} interface Tree { x: number; @@ -36,7 +35,7 @@ function useNoise3d( ); } -function useNoise4d( +export function useNoise4d( position: Vector2, ticker: Ticker, { scaleT = 1, scaleXy = 1 }: NoiseOpts = {}, @@ -178,49 +177,80 @@ function Tree({ const leanLeft = Math.sqrt(constrain(0, 1, splitLeft)); const leanRight = Math.sqrt(constrain(0, 1, splitRight)); - const leftWidth = lerp(tree.width, tree.width * 0.3, leanLeft); - const rightWidth = lerp(tree.width, tree.width * 0.3, leanRight); - - const leftX = tree.x - tree.width / 2 + leftWidth / 2; - const rightX = tree.x + tree.width / 2 - rightWidth / 2; + const splitY = baseY - splitPoint; left = ( - - + ); - right = ( - - + ); + + // const leftWidth = lerp(tree.width, tree.width * 0.3, leanLeft); + // const rightWidth = lerp(tree.width, tree.width * 0.3, leanRight); + + // const leftX = tree.x - tree.width / 2 + leftWidth / 2; + // const rightX = tree.x + tree.width / 2 - rightWidth / 2; + + // left = ( + // + // + // + // ); + + // right = ( + // + // + // + // ); } return ( @@ -240,6 +270,41 @@ function Tree({ ); } +function Branch({ + tree, + xy, + t, + lean, +}: { + tree: Tree; + xy: Vector2; + t: Ticker; + lean: number; +}) { + const angleDeg = mapRange(-1, 1, 20, 60, useNoise3d(xy, t)); + const angleRad = degToRad(angleDeg); + const r = mapRange(-1, 1, 5, 10, useNoise3d(xy, t)); + + const lowCenter = new Vector2(r, 0); + const arcLength = r * angleRad; + const amtOfLowArc = constrain(0, 1, invLerp(0, arcLength, tree.height)); + + const target = Vector2.ZERO.rotateAround(lowCenter, angleRad * amtOfLowArc); + + const path = new SvgPathBuilder() + .moveTo(0, tree.width / 2) + .arcTo(r, r, 0, false, true, target); + + return ( + + ); +} + function useSpring( ticker: Ticker, target: number, diff --git a/src/trees/trees-main.tsx b/src/trees/trees-main.tsx index 3063e27b..2d5136f4 100644 --- a/src/trees/trees-main.tsx +++ b/src/trees/trees-main.tsx @@ -1,6 +1,19 @@ import { assertExists } from "@/lib/assert"; +import { LeafPatternApp } from "@/trees/LeafPattern"; import { TreesApp } from "@/trees/TreesApp"; import { createRoot } from "react-dom/client"; +import { RouterProvider, createHashRouter } from "react-router-dom"; + +const router = createHashRouter([ + { + path: "/patterns", + element: , + }, + { + path: "*", + element: , + }, +]); const root = assertExists(document.getElementById("root")); -createRoot(root).render(); +createRoot(root).render(); diff --git a/yarn.lock b/yarn.lock index 8e896b1e..d10efa16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1631,6 +1631,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.14.1": + version: 1.14.1 + resolution: "@remix-run/router@npm:1.14.1" + checksum: caed61639006444a66ca832f1e500bac2fcf02695183e967ff1452d3172f888f2bb40591b239c85f9003b9628383cfd4c8ef55cde800d14276905c7031c9f0b9 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^4.2.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" @@ -6149,6 +6156,30 @@ __metadata: languageName: node linkType: hard +"react-router-dom@npm:^6.21.1": + version: 6.21.1 + resolution: "react-router-dom@npm:6.21.1" + dependencies: + "@remix-run/router": "npm:1.14.1" + react-router: "npm:6.21.1" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 2d75bd889828fa5516ad076b44506656d826c365645e7079138cd0ef899db28a1b212f708a6c6e3b543ae11b96b2031f01201cc2fe1733dd4d9c5cbdd4d734ef + languageName: node + linkType: hard + +"react-router@npm:6.21.1": + version: 6.21.1 + resolution: "react-router@npm:6.21.1" + dependencies: + "@remix-run/router": "npm:1.14.1" + peerDependencies: + react: ">=16.8" + checksum: 1220cc75e0c915a26dde9dbb6509a8f0b0163d96e5ad591af91d9bb5a92a18401718f8d872a03d1cb366e7a6216c165a5cadd12375adf97943f37d7f5c487a90 + languageName: node + linkType: hard + "react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" @@ -6931,6 +6962,7 @@ __metadata: react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-icons: "npm:^4.12.0" + react-router-dom: "npm:^6.21.1" react-transition-group: "npm:^4.4.5" rebound: "npm:^0.1.0" regenerator-runtime: "npm:^0.14.0"