diff --git a/client/src/components/inspector/TraceRenderer.tsx b/client/src/components/inspector/TraceRenderer.tsx
index 926feacf..7f11b735 100644
--- a/client/src/components/inspector/TraceRenderer.tsx
+++ b/client/src/components/inspector/TraceRenderer.tsx
@@ -154,8 +154,8 @@ export function TraceRenderer({
) : (
<>
- {layers.map((l) => (
-
+ {layers.map((l, i) => (
+
))}
>
diff --git a/client/src/components/layer-editor/LayerEditor.tsx b/client/src/components/layer-editor/LayerEditor.tsx
index b6db2c7a..5fea4dbb 100644
--- a/client/src/components/layer-editor/LayerEditor.tsx
+++ b/client/src/components/layer-editor/LayerEditor.tsx
@@ -27,6 +27,35 @@ import {
import { Layer } from "slices/layers";
import { inferLayerName, layerHandlers } from "../../layers/Layer";
+const compositeOperations = [
+ "color",
+ "color-burn",
+ "color-dodge",
+ "copy",
+ "darken",
+ "destination-atop",
+ "destination-in",
+ "destination-out",
+ "destination-over",
+ "difference",
+ "exclusion",
+ "hard-light",
+ "hue",
+ "lighten",
+ "lighter",
+ "luminosity",
+ "multiply",
+ "overlay",
+ "saturation",
+ "screen",
+ "soft-light",
+ "source-atop",
+ "source-in",
+ "source-out",
+ "source-over",
+ "xor",
+];
+
type LayerEditorProps = {
value: Layer;
onValueChange?: (v: Layer) => void;
@@ -126,21 +155,27 @@ function Component(
"Transparency",
({
+ items={["0", "25", "50", "75"].map((c) => ({
id: c,
name: `${c}%`,
}))}
- value="100"
+ value={draft.transparency ?? "0"}
showArrow
+ onChange={(e) =>
+ setDraft?.(produce(draft, (d) => set(d, "transparency", e)))
+ }
/>
)}
{renderOption(
"Display Mode",
+ setDraft?.(produce(draft, (d) => set(d, "displayMode", e)))
+ }
/>
)}
{renderHeading("Source Options")}
diff --git a/client/src/hooks/useBreakpoints.tsx b/client/src/hooks/useBreakpoints.tsx
index e5cfde5c..c4736297 100644
--- a/client/src/hooks/useBreakpoints.tsx
+++ b/client/src/hooks/useBreakpoints.tsx
@@ -6,7 +6,6 @@ import { useMemo } from "react";
import { UploadedTrace } from "slices/UIState";
import { useLayer } from "slices/layers";
-
export type Comparator = {
key: string;
apply: (value: number, reference: number) => boolean;
@@ -30,7 +29,8 @@ export type DebugLayerData = {
};
export function useBreakpoints(key?: string) {
const { layer } = useLayer(key);
- const {monotonicF, monotonicG,breakpoints ,code,trace} = layer?.source??{}
+ const { monotonicF, monotonicG, breakpoints, code, trace } =
+ layer?.source ?? {};
// TODO:
return useMemo(() => {
const memo = keyBy(trace?.content?.events, "id");
@@ -53,7 +53,7 @@ export function useBreakpoints(key?: string) {
type,
property = "",
reference = 0,
- } of breakpoints??[]) {
+ } of breakpoints ?? []) {
const isType = !type || type === event.type;
const match = condition?.apply?.(get(event, property), reference);
if (active && isType && match) {
@@ -69,7 +69,7 @@ export function useBreakpoints(key?: string) {
call(code ?? "", "shouldBreak", [
step,
event,
- trace?.content?.events?? [],
+ trace?.content?.events ?? [],
])
) {
return { result: "Script editor" };
@@ -81,5 +81,4 @@ export function useBreakpoints(key?: string) {
return { result: "" };
});
}, [code, trace?.content, breakpoints, monotonicF, monotonicG]);
-
}
diff --git a/client/src/layers/Layer.tsx b/client/src/layers/Layer.tsx
index 6795c4dd..8cf651aa 100644
--- a/client/src/layers/Layer.tsx
+++ b/client/src/layers/Layer.tsx
@@ -18,7 +18,7 @@ export type SelectionInfoProvider = FC<{
export type LayerController = {
key: K;
editor: FC>>;
- renderer: FC<{ layer?: Layer }>;
+ renderer: FC<{ layer?: Layer; index?: number }>;
service?: FC>>;
inferName: (layer: Layer) => string;
steps: FC<{
@@ -28,12 +28,19 @@ export type LayerController = {
getSelectionInfo?: SelectionInfoProvider;
};
-export function RenderLayer({ layer }: { layer?: Layer }) {
+export function RenderLayer({
+ layer,
+ index,
+}: {
+ layer?: Layer;
+ index?: number;
+}) {
return (
<>
{layer &&
createElement(layerHandlers[layer?.source?.type ?? ""]?.renderer, {
layer,
+ index,
})}
>
);
diff --git a/client/src/layers/map/index.tsx b/client/src/layers/map/index.tsx
index d7c9608f..9b72b652 100644
--- a/client/src/layers/map/index.tsx
+++ b/client/src/layers/map/index.tsx
@@ -7,7 +7,7 @@ import { useEffectWhen } from "hooks/useEffectWhen";
import { useMapContent } from "hooks/useMapContent";
import { useParsedMap } from "hooks/useParsedMap";
import { LayerController, inferLayerName } from "layers";
-import { isUndefined, round, set, startCase } from "lodash";
+import { isUndefined, map, round, set, startCase } from "lodash";
import { withProduce } from "produce";
import { useMemo } from "react";
import { Map } from "slices/UIState";
@@ -41,9 +41,22 @@ export const controller = {
>
);
}),
- renderer: ({ layer }) => {
+ renderer: ({ layer, index }) => {
const { nodes } = layer?.source?.parsedMap ?? {};
- const nodes2 = useMemo(() => [nodes ?? []], [nodes]);
+ const nodes2 = useMemo(
+ () => [
+ map(nodes, (n) => ({
+ ...n,
+ meta: {
+ ...n.meta,
+ sourceLayerIndex: index,
+ sourceLayerAlpha: 1 - 0.01 * +(layer?.transparency ?? 0),
+ sourceLayerDisplayMode: layer?.displayMode ?? "source-over",
+ },
+ })),
+ ],
+ [nodes, index, layer?.transparency, layer?.displayMode]
+ );
return ;
},
steps: ({ children }) => <>{children?.([])}>,
diff --git a/client/src/layers/trace/index.tsx b/client/src/layers/trace/index.tsx
index c43ac307..02fe2069 100644
--- a/client/src/layers/trace/index.tsx
+++ b/client/src/layers/trace/index.tsx
@@ -100,7 +100,8 @@ export type TraceLayerData = {
trace?: UploadedTrace;
parsedTrace?: ParseTraceWorkerReturnType;
onion?: "off" | "transparent" | "solid";
-} & PlaybackLayerData & DebugLayerData;
+} & PlaybackLayerData &
+ DebugLayerData;
export type TraceLayer = Layer;
@@ -178,24 +179,54 @@ export const controller = {
>
);
}),
- renderer: ({ layer }) => {
+ renderer: ({ layer, index }) => {
const parsedTrace = layer?.source?.parsedTrace;
const step = useThrottle(layer?.source?.step ?? 0, 1000 / 60);
- const path = use2DPath(layer, step);
+ const path = use2DPath(layer, index, step);
const steps = useMemo(
() =>
map(parsedTrace?.stepsPersistent, (c) =>
- map(c, (d) => merge(d, { meta: { sourceLayer: layer?.key } }))
+ map(c, (d) =>
+ merge(d, {
+ meta: {
+ sourceLayer: layer?.key,
+ sourceLayerIndex: index,
+ sourceLayerAlpha: 1 - 0.01 * +(layer?.transparency ?? 0),
+ sourceLayerDisplayMode: layer?.displayMode ?? "source-over",
+ },
+ })
+ )
),
- [parsedTrace?.stepsPersistent, layer?.key]
+ [
+ parsedTrace?.stepsPersistent,
+ layer?.key,
+ layer?.transparency,
+ layer?.displayMode,
+ index,
+ ]
);
const steps1 = useMemo(
() =>
map(parsedTrace?.stepsTransient, (c) =>
- map(c, (d) => merge(d, { meta: { sourceLayer: layer?.key } }))
+ map(c, (d) =>
+ merge(d, {
+ meta: {
+ sourceLayer: layer?.key,
+ sourceLayerIndex: index,
+ sourceLayerAlpha: 1 - 0.01 * +(layer?.transparency ?? 0),
+ sourceLayerDisplayMode: layer?.displayMode ?? "source-over",
+ },
+ })
+ )
),
- [parsedTrace?.stepsTransient, layer?.key]
+ [
+ parsedTrace?.stepsTransient,
+ layer?.key,
+ layer?.transparency,
+ layer?.displayMode,
+ index,
+ ]
);
const steps2 = useMemo(() => [steps1[step] ?? []], [steps1, step]);
return (
@@ -217,11 +248,11 @@ export const controller = {
.filter((c) => c.meta?.sourceLayer === layer?.key)
.map((c) => c.meta?.step)
.filter(negate(isUndefined))
- .sort((a, b) => a - b)
+ .sort((a, b) => a! - b!)
.value() as number[];
const info = chain(event?.info?.components)
.filter((c) => c.meta?.sourceLayer === layer?.key)
- .filter((c) => c.meta.info)
+ .filter((c) => c.meta?.info)
.value() as any[];
if (steps.length && layer) {
const step = last(steps)!;
@@ -271,7 +302,7 @@ export const controller = {
},
} satisfies LayerController<"trace", TraceLayerData>;
-function use2DPath(layer?: TraceLayer, step: number = 0) {
+function use2DPath(layer?: TraceLayer, index: number = 0, step: number = 0) {
const { palette } = useTheme();
const { getPath } = useMemo(
() =>
@@ -336,7 +367,7 @@ function use2DPath(layer?: TraceLayer, step: number = 0) {
nodes={[
map(primitive, (c) => ({
component: c,
- meta: { source: "path" },
+ meta: { source: "path", sourceLayerIndex: -99999 + index },
})),
]}
/>
@@ -344,6 +375,6 @@ function use2DPath(layer?: TraceLayer, step: number = 0) {
}
}
return <>>;
- }, [layer, step, palette, getPath]);
+ }, [layer, index, step, palette, getPath]);
return element;
}
diff --git a/client/src/pages/StepsPage.tsx b/client/src/pages/StepsPage.tsx
index b7a01e95..37f7e2a2 100644
--- a/client/src/pages/StepsPage.tsx
+++ b/client/src/pages/StepsPage.tsx
@@ -15,10 +15,17 @@ import { Placeholder } from "components/inspector/Placeholder";
import { useViewTreeContext } from "components/inspector/ViewTree";
import { usePlaybackState } from "hooks/usePlaybackState";
import { inferLayerName, layerHandlers } from "layers/Layer";
-import { defer, map } from "lodash";
+import { defer, map, throttle } from "lodash";
import { Page } from "pages/Page";
import { TraceEvent } from "protocol";
-import { cloneElement, createElement, useEffect, useMemo, useRef } from "react";
+import {
+ cloneElement,
+ createElement,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+} from "react";
import { useLayer } from "slices/layers";
const divider = ;
@@ -40,18 +47,23 @@ export function StepsPage() {
}
}, [layer]);
- useEffect(() => {
- defer(
- () =>
+ const f = useCallback(
+ throttle(
+ (step: number) =>
ref?.current?.scrollToIndex?.({
index: step,
align: "start",
behavior: "smooth",
offset: -pxToInt(spacing(6 + 2)),
}),
- 0
- );
- }, [step, playing, spacing]);
+ 1000 / 30
+ ),
+ [ref]
+ );
+
+ useEffect(() => {
+ defer(() => f(step));
+ }, [f, step]);
return (
diff --git a/client/src/public-dev/manifest.json b/client/src/public-dev/manifest.json
index 38c9ddcd..dda55067 100644
--- a/client/src/public-dev/manifest.json
+++ b/client/src/public-dev/manifest.json
@@ -1,9 +1,9 @@
{
"short_name": "Visualiser",
"name": "Visualiser",
- "version": "1.0.5",
+ "version": "dev",
"description": "Visualise pathfinding search and more",
- "version_name": "1.0.5; mid October 2023",
+ "version_name": "dev",
"repository": "https://github.com/path-visualiser/app",
"docs": "https://github.com/path-visualiser/app/blob/master/docs",
"icons": [
diff --git a/client/src/public/manifest.json b/client/src/public/manifest.json
index 728c06b0..8c284d27 100644
--- a/client/src/public/manifest.json
+++ b/client/src/public/manifest.json
@@ -1,9 +1,9 @@
{
"short_name": "Visualiser",
"name": "Visualiser",
- "version": "1.1.0",
+ "version": "1.1.1",
"description": "Visualise pathfinding search and more",
- "version_name": "1.1.0; mid November 2023",
+ "version_name": "1.1.1; early December 2023",
"repository": "https://github.com/path-visualiser/app",
"docs": "https://github.com/path-visualiser/app/blob/master/docs",
"icons": [
diff --git a/client/src/slices/layers.ts b/client/src/slices/layers.ts
index 5c32d6de..ee39b726 100644
--- a/client/src/slices/layers.ts
+++ b/client/src/slices/layers.ts
@@ -9,6 +9,7 @@ export type Layer> = {
name?: string;
source?: { type: string } & T;
transparency?: "25" | "50" | "75" | "100";
+ displayMode?: GlobalCompositeOperation;
};
export type Layers = {
diff --git a/internal-renderers/src/d2-renderer/D2Renderer.ts b/internal-renderers/src/d2-renderer/D2Renderer.ts
index 8474fd3f..81b5c63d 100644
--- a/internal-renderers/src/d2-renderer/D2Renderer.ts
+++ b/internal-renderers/src/d2-renderer/D2Renderer.ts
@@ -135,6 +135,7 @@ class D2Renderer
add(components: ComponentEntry[]) {
const id = nanoid();
+ map(this.#workers, (w) => w.call("add", [components, id]));
const bodies = map(components, ({ component, meta }) => ({
...primitives[component.$].test(component),
component,
@@ -142,9 +143,6 @@ class D2Renderer
index: this.#next(),
}));
this.#system.load(bodies);
- map(this.#workers, (w) =>
- w.call("add", [map(components, "component"), id])
- );
return () =>
defer(() => {
for (const c of bodies) this.#system.remove(c);
diff --git a/internal-renderers/src/d2-renderer/D2RendererWorker.ts b/internal-renderers/src/d2-renderer/D2RendererWorker.ts
index c9c0ab75..98e7dce5 100644
--- a/internal-renderers/src/d2-renderer/D2RendererWorker.ts
+++ b/internal-renderers/src/d2-renderer/D2RendererWorker.ts
@@ -2,11 +2,14 @@ import combinate from "combinate";
import {
Dictionary,
ceil,
+ chain as _,
debounce,
floor,
+ head,
isEqual,
map,
once,
+ pick,
range,
shuffle,
sortBy,
@@ -130,10 +133,16 @@ export class D2RendererWorker extends EventEmitter<
#cache: { [K in string]: { hash: string; tile: ImageBitmap } } = {};
- add(component: CompiledD2IntrinsicComponent[], id: string) {
- const bodies = map(component, (c) => ({
- ...primitives[c.$].test(c),
- component: c,
+ add(components: ComponentEntry[], id: string) {
+ const bodies = map(components, ({ component, meta }) => ({
+ ...primitives[component.$].test(component),
+ component,
+ meta: pick(
+ meta,
+ "sourceLayerIndex",
+ "sourceLayerAlpha",
+ "sourceLayerDisplayMode"
+ ),
index: this.#next(),
}));
this.#system.load(bodies);
@@ -241,13 +250,28 @@ export class D2RendererWorker extends EventEmitter<
length
);
- for (const { component } of bodies) {
- draw(component, ctx, {
- scale,
- x: -left * scale.x,
- y: -top * scale.y,
- });
- }
+ _(bodies)
+ .sortBy((c) => -(c.meta?.sourceLayerIndex ?? 0))
+ .groupBy((c) => c.meta?.sourceLayerIndex ?? 0)
+ .forEach((group) => {
+ const g2 = new OffscreenCanvas(tile.width, tile.height);
+ const ctx2 = g2.getContext("2d")!;
+ for (const { component } of group) {
+ draw(component, ctx2, {
+ scale,
+ x: -left * scale.x,
+ y: -top * scale.y,
+ });
+ }
+ const alpha = head(group)?.meta?.sourceLayerAlpha ?? 1;
+ const displayMode =
+ head(group)?.meta?.sourceLayerDisplayMode ?? "source-over";
+ ctx.globalCompositeOperation = displayMode;
+ ctx.globalAlpha = alpha;
+ ctx.drawImage(g2, 0, 0);
+ })
+ .value();
+
const bitmap = g.transferToImageBitmap();
this.#cache[tileKey] = { hash: nextHash, tile: bitmap };
diff --git a/renderer/Renderer.ts b/renderer/Renderer.ts
index 3d691c96..cd699797 100644
--- a/renderer/Renderer.ts
+++ b/renderer/Renderer.ts
@@ -21,7 +21,14 @@ export type RemoveElementCallback = () => void;
export type ComponentEntry<
V extends CompiledComponent = CompiledComponent,
- M = any
+ M = {
+ sourceLayer?: string;
+ sourceLayerIndex?: number;
+ sourceLayerDisplayMode?: GlobalCompositeOperation;
+ sourceLayerAlpha?: number;
+ step?: number;
+ info?: any;
+ }
> = {
component: V;
meta?: M;