Skip to content

Commit

Permalink
main: leaf pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
SomeHats committed Jan 19, 2024
1 parent 1a49977 commit ac42036
Show file tree
Hide file tree
Showing 16 changed files with 676 additions and 80 deletions.
Binary file modified .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/infinite-scroll/CaterpillarScroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ function WormyScrollbar({
3.5,
2,
0,
0,
0,
false,
false,
lineBasePx + 1,
effectiveTopPx + effectiveHeightPx + 2,
)
Expand Down
60 changes: 36 additions & 24 deletions src/lib/DebugSvg.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import { scale } from "@/blob-tree/canvas";

Check warning on line 1 in src/lib/DebugSvg.tsx

View workflow job for this annotation

GitHub Actions / build

'scale' is defined but never used. Allowed unused vars must match /^_/u
import {
DebugDraw,
FillOptions,
StrokeAndFillOptions,
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";

function asProps<El extends keyof JSX.IntrinsicElements>() {
return <T extends ComponentProps<El>>(props: T) => props;
}

function getStrokeProps({
function useStrokeProps({
strokeWidth = 1,
stroke = "transparent",
strokeCap = "butt",
strokeDash = [],
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,
});
}

Expand All @@ -36,9 +39,9 @@ function getFillProps({ fill = "transparent" }: FillOptions = {}) {
});
}

function getStrokeAndFillProps(options: StrokeAndFillOptions) {
function useStrokeAndFillProps(options: StrokeAndFillOptions) {
return {
...getStrokeProps(options),
...useStrokeProps(options),
...getFillProps(options),
};
}
Expand All @@ -58,24 +61,30 @@ 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 (
<text
x={adjustedPosition.x}
y={adjustedPosition.y}
className="font-sans"
textAnchor="middle"
fontSize={8}
textAnchor="left"
fontSize={8 / scale}
{...getFillProps({ fill: color ?? DebugDraw.DEFAULT_DEBUG_COLOR })}
>
{label}
</text>
);
}

interface DebugOptions { color?: string; label?: string }
interface DebugOptions {
color?: string;
label?: string;
}

export function DebugSvgPath({
color,
Expand All @@ -87,7 +96,7 @@ export function DebugSvgPath({
return (
<path
d={path}
{...getStrokeProps(getDebugStrokeOptions(color))}
{...useStrokeProps(getDebugStrokeOptions(color))}
fill="transparent"
/>
);
Expand All @@ -97,27 +106,28 @@ export function DebugPointX({
position,
...debugOpts
}: { position: Vector2Ish } & DebugOptions) {
const scale = useSvgScale();
const { x, y } = Vector2.from(position);
return (
<>
<DebugSvgPath
{...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()}
/>
Expand All @@ -130,14 +140,15 @@ export function DebugPointO({
position,
...debugOpts
}: { position: Vector2Ish } & DebugOptions) {
const scale = useSvgScale();
const { x, y } = Vector2.from(position);
return (
<>
<circle
cx={x}
cy={y}
r={DebugDraw.DEBUG_POINT_SIZE}
{...getStrokeProps(getDebugStrokeOptions(debugOpts.color))}
r={DebugDraw.DEBUG_POINT_SIZE / scale}
{...useStrokeProps(getDebugStrokeOptions(debugOpts.color))}
/>
<DebugLabel {...debugOpts} position={position} />
</>
Expand All @@ -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 (
Expand Down Expand Up @@ -208,7 +220,7 @@ export function DebugCircle({
cx={x}
cy={y}
r={radius}
{...getStrokeAndFillProps(
{...useStrokeAndFillProps(
getDebugStrokeOptions(debugOpts.color),
)}
/>
Expand Down
15 changes: 15 additions & 0 deletions src/lib/geom/AABB.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Vector2 } from "@/lib/geom/Vector2";
import { mapRange } from "@/lib/utils";

Check warning on line 2 in src/lib/geom/AABB.ts

View workflow job for this annotation

GitHub Actions / build

'mapRange' is defined but never used. Allowed unused vars must match /^_/u

export default class AABB {
static fromLeftTopRightBottom(
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/geom/CirclePathSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
}
Expand Down
27 changes: 24 additions & 3 deletions src/lib/geom/Vector2.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
);
}
}
105 changes: 105 additions & 0 deletions src/lib/react/Svg.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(1);
export function useSvgScale() {
return useContext(SvgScaleContext);
}

export const Svg = forwardRef(function Svg(
{
viewBox,
...props
}: Omit<ComponentProps<"svg">, "viewBox"> & { viewBox: AABB },
ref: ForwardedRef<SVGSVGElement>,
) {
const [svg, setSvg] = useState<SVGSVGElement | null>(null);
const size = useResizeObserver(svg, sizeFromContentRect);

const scale = size ? size.x / viewBox.width : 1;

return (
<SvgScaleContext.Provider value={scale}>
<svg
{...props}
ref={useMergedRefs(setSvg, ref)}
viewBox={`${viewBox.left} ${viewBox.top} ${viewBox.right} ${viewBox.bottom}`}
/>
</SvgScaleContext.Provider>
);
});

export function SvgApp({
viewBox,
onPointerDown,
onPointerMove,
onPointerUp,
onPointerCancel,
...props
}: Omit<
ComponentProps<typeof Svg>,
"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<SVGSVGElement>(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 <Svg ref={svgRef} viewBox={viewBox} {...props} />;
}
Loading

0 comments on commit ac42036

Please sign in to comment.