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;