diff --git a/src/components/ColorScatterPlot.svelte b/src/components/ColorScatterPlot.svelte index 2ff153de..b1e61335 100644 --- a/src/components/ColorScatterPlot.svelte +++ b/src/components/ColorScatterPlot.svelte @@ -4,11 +4,12 @@ import { makePosAndSizes, toggleElement, - clampToRange, selectColorsFromDragZ, selectColorsFromDrag, toXY, makeScales, + dragEventToColorZ, + dragEventToColorXY, } from "../lib/utils"; import configStore from "../stores/config-store"; import { scaleLinear } from "d3-scale"; @@ -82,141 +83,8 @@ $: pos = makePosAndSizes(pickedColors); let dragging: false | { x: number; y: number } = false; - let dragBox: false | { x: number; y: number } = false; - let parentPos = { x: 0, y: 0 }; - - const eventToColorXY = ( - e: any, - color: Color, - originalColor: Color - ): Color => { - if (!dragging || dragging.x === undefined || dragging.y === undefined) - return color; - - const values = toXY(e); - const screenPosDelta = { - x: values.x - dragging.x, - y: values.y - dragging.y, - }; - - const xClamp = (v: number) => clampToRange(v, config.xDomain); - const yClamp = (v: number) => clampToRange(v, config.yDomain); - // screen coordinates - const newPos = [ - x(originalColor) + screenPosDelta.x, - y(originalColor) + screenPosDelta.y, - ]; - // color space coordinates - const newVal = [ - xClamp(xInv(newPos[0], newPos[1])), - yClamp(yInv(newPos[0], newPos[1])), - ]; - const coords = originalColor.toChannels(); - coords[config.xChannelIndex] = newVal[0]; - coords[config.yChannelIndex] = newVal[1]; - return Color.colorFromChannels(coords, colorSpace); - }; - - const eventToColorZ = (e: any, color: Color, originalColor: Color): Color => { - if (!dragging || dragging.x === undefined || dragging.y === undefined) - return color; - - const screenPosDelta = toXY(e).y - dragging.y; - const coords = originalColor.toChannels(); - const zClamp = (v: number) => clampToRange(v, config.zDomain); - coords[config.zChannelIndex] = zClamp( - zInv(z(originalColor) + screenPosDelta) - ); - - return Color.colorFromChannels(coords, colorSpace); - }; - - function stopDrag() { - dragging = false; - dragBox = false; - } let originalColors = [] as Color[]; - let isMainBoxDrag = true; - let isPointDrag = false; - const startDrag = (isXYDrag: boolean, idx?: number) => (e: any) => { - if (scatterPlotMode !== "moving") return; - hoveredPoint = false; - // console.log("start drag", e.target); - startDragging(); - const targetIsPoint = typeof idx === "number"; - let target = e.target; - const isMetaKey = e.metaKey || e.shiftKey || e.ctrlKey; - isPointDrag = false; - if (targetIsPoint && !focusSet.has(idx)) { - const newSet = isMetaKey ? [...focusSet, idx] : [idx]; - onFocusedColorsChange(newSet); - isPointDrag = true; - } - - isMainBoxDrag = isXYDrag; - parentPos = target.getBoundingClientRect(); - const { x, y } = toXY(e); - dragging = { x, y }; - originalColors = [...colors]; - - if (focusedColors.length === 0) { - dragBox = { x, y }; - } else { - dragBox = false; - } - }; - - type Func = typeof eventToColorZ | typeof eventToColorZ; - const dragResponse = (func: Func) => (e: any) => { - const newColors = colors.map((color, idx) => - focusSet.has(idx) ? func(e, color, originalColors[idx]) : color - ); - - onColorsChange(newColors); - }; - - const rectMoveResponse = (isZ: boolean) => (e: any) => { - // console.log("rect move response"); - if (!dragging || scatterPlotMode !== "moving") return; - const { x, y } = toXY(e); - if (dragging && focusedColors.length > 0) { - dragResponse(isZ ? eventToColorZ : eventToColorXY)(e); - } - if (dragging && focusedColors.length === 0) { - dragBox = { x, y }; - } - }; - - const rectMoveEnd = (isZ: boolean) => (e: any) => { - // console.log("rect move end"); - stopDragging(); - if (scatterPlotMode !== "moving") return; - if (!isPointDrag && dragBox && dragging) { - const dragResp = isZ ? selectColorsFromDragZ : selectColorsFromDrag; - const newFocus = dragResp(dragBox, dragging, parentPos, colors, { - x, - y, - z, - isPolar: config.isPolar, - plotHeight, - plotWidth, - }); - onFocusedColorsChange(newFocus); - } - const xy = toXY(e); - if ( - !isPointDrag && - dragging && - dragging.x === xy.x && - dragging.y === xy.y - ) { - onFocusedColorsChange([]); - } - - dragging = false; - dragBox = false; - }; $: luminance = bg.luminance(); $: axisColor = luminance > 0.4 ? "#00000022" : "#ffffff55"; @@ -225,8 +93,8 @@ let hoveredPoint: Color | false = false; let hoverPoint = (x: typeof hoveredPoint) => (hoveredPoint = x); + // coordinate transforms - // slight indirection to make these mappings more re-usable $: scales = makeScales( { rScale, angleScale, xScale, yScale, zScale }, config @@ -235,27 +103,15 @@ $: x = scales.x; $: y = scales.y; $: z = scales.z; - // screen -> color space - $: xInv = scales.xInv; - $: yInv = scales.yInv; - $: zInv = scales.zInv; - function clickResponse(e: any, i: number) { - if (e.metaKey || e.shiftKey) { - onFocusedColorsChange(toggleElement(focusedColors, i)); - } else { - onFocusedColorsChange([i]); - } - } - - let CircleProps = (color: Color, i: number) => ({ + $: CircleProps = (color: Color, i: number) => ({ cx: x(color), cy: y(color), r: 10, class: "cursor-pointer", fill: color.toDisplay(), }); - let RectProps = (color: Color, i: number) => ({ + $: RectProps = (color: Color, i: number) => ({ y: z(color), class: "cursor-pointer", fill: color.toDisplay(), @@ -263,12 +119,12 @@ height: 5, width: 80 - 10 * 2 + (focusSet.has(i) ? 10 : 0), }); - $: SelectionBoxStyles = { + $: outerDottedBoxStyle = { fill: "white", "fill-opacity": "0", - "pointer-events": "none", "stroke-dasharray": "5,5", "stroke-width": "1", + "stroke-opacity": focusedColors.length > 1 ? "1" : "0", cursor: "grab", stroke: selectionColor, }; @@ -280,21 +136,124 @@ axisColor, textColor, colorSpace, - dragging: !!dragging, + dragging: interactionMode === "drag", + }; + + let interactionMode: "idle" | "drag" | "select" | "point-touch" = "idle"; + let dragTargetPos = { x: 0, y: 0 }; + let dragDelta = { x: 0, y: 0 }; + function dragStart(e: any) { + startDragging(); + interactionMode = "drag"; + // console.log("drag start"); + const { x, y } = toXY(e); + dragTargetPos = e.target.getBoundingClientRect(); + dragDelta = { x: dragTargetPos.x - x, y: dragTargetPos.y - y }; + originalColors = [...colors]; + } + + const dragUpdate = (isZ: boolean) => (e: any) => { + // console.log("drag update"); + const dragToColor = isZ ? dragEventToColorZ : dragEventToColorXY; + const newColors = colors.map((color, idx) => { + if (!focusSet.has(idx)) { + return color; + } + return dragToColor( + e, + originalColors[idx], + dragTargetPos, + config, + scales, + colorSpace, + dragDelta + ); + }); + + onColorsChange(newColors); }; + function dragEnd(e: any) { + interactionMode = "idle"; + stopDragging(); + // console.log("drag end"); + } - function dragStart() {} - function dragUpdate() {} - function dragEnd() {} - function selectionStart() {} - function selectionUpdate() {} - function selectionEnd() {} - const clickPoint = (i: number) => (e: any) => { + let selectionMouseStart = { x: 0, y: 0 }; + let selectionMouseCurrent = { x: 0, y: 0 }; + let windowPos = { x: 0, y: 0 }; + let selectionIsZ = false; + const selectionStart = (isZ: boolean) => (e: any) => { + selectionIsZ = isZ; + interactionMode = "select"; + // console.log("selection start"); + const { x, y } = toXY(e); + selectionMouseStart = { x, y }; + selectionMouseCurrent = { x, y }; + windowPos = e.target.getBoundingClientRect(); + }; + const selectionUpdate = (isZ: boolean) => (e: any) => { + // console.log("selection update"); + const { x, y } = toXY(e); + selectionMouseCurrent = { x, y }; + }; + const selectionEnd = (isZ: boolean) => (e: any) => { + interactionMode = "idle"; + const miniConfig = { isPolar: config.isPolar, plotHeight, plotWidth }; + const resp = isZ ? selectColorsFromDragZ : selectColorsFromDrag; + const newFocus = resp( + selectionMouseStart, + selectionMouseCurrent, + windowPos, + colors, + { ...miniConfig, ...scales } + ); + onFocusedColorsChange(newFocus); + }; + + const pointMouseUp = (i: number) => (e: any) => { + // console.log("point mouse up"); + interactionMode = "idle"; const isMeta = e.metaKey || e.shiftKey; const newElements = isMeta ? toggleElement(focusedColors, i) : [i]; onFocusedColorsChange(newElements); }; + + const switchToDragPoint = (i: number) => (e: any) => { + if (interactionMode !== "point-touch") return; + startDragging(); + // console.log("switch to drag point"); + onFocusedColorsChange([i]); + interactionMode = "drag"; + }; + + const fillParamsXY = { + x: 0, + y: 0, + width, + height, + opacity: 0, + fill: "white", + }; + const fillParamsZ = { + x: margin.left, + // y: margin.top, + y: 0, + width: 80, + height, + opacity: 0, + fill: "white", + }; + + $: selectionBoxStyle = { + x: Math.min(selectionMouseStart.x, selectionMouseCurrent.x) - windowPos.x, + y: Math.min(selectionMouseStart.y, selectionMouseCurrent.y) - windowPos.y, + width: Math.abs(selectionMouseStart.x - selectionMouseCurrent.x), + height: Math.abs(selectionMouseStart.y - selectionMouseCurrent.y), + fill: selectionColor, + "fill-opacity": "0.5", + class: "pointer-events-none", + }; @@ -314,16 +273,10 @@ {/if} - - - + {#each colors as color, i} - - - {#if scatterPlotMode === "moving"} { + dragStart(e); + interactionMode = "point-touch"; + }} + on:mouseup|preventDefault={pointMouseUp(i)} + on:mouseleave|preventDefault={switchToDragPoint(i)} /> {/if} {#if scatterPlotMode === "looking"} @@ -369,13 +321,18 @@ {/if} {/each} {#each blindColors as blindColor, i} - { + dragStart(e); + interactionMode = "point-touch"; + }} + on:mouseup|preventDefault={pointMouseUp(i)} + on:mouseleave|preventDefault={switchToDragPoint(i)} /> {/each} @@ -388,30 +345,45 @@ {hoveredPoint.toDisplay()} {/if} - {#if pickedColors.length > 1} + + {#if focusedColors.length > 0} dragStart(e)} + on:mousedown|preventDefault={(e) => dragStart(e)} /> {/if} - - - {#if dragging && dragBox && isMainBoxDrag} - - {/if} + {#if interactionMode === "select"} + + {#if !selectionIsZ} + + {/if} + + + {/if} + {#if interactionMode === "drag"} + + dragEnd(e)} + on:mouseup|preventDefault={(e) => dragEnd(e)} + /> + {/if} @@ -421,85 +393,79 @@
- - + + - - - {#each colors as color, i} - clickResponse(e, i)} - on:mousedown|preventDefault={startDrag(false, i)} - on:touchstart|preventDefault={startDrag(false, i)} - on:touchend|preventDefault={() => onFocusedColorsChange([i])} + on:mousedown|preventDefault={(e) => { + dragStart(e); + interactionMode = "point-touch"; + }} + on:mouseup|preventDefault={pointMouseUp(i)} + on:mouseleave|preventDefault={switchToDragPoint(i)} /> {/each} {#each blindColors as color, i} - clickResponse(e, i)} - on:mousedown|preventDefault={startDrag(false, i)} - on:touchstart|preventDefault={startDrag(false, i)} - on:touchend|preventDefault={() => onFocusedColorsChange([i])} + on:mousedown|preventDefault={(e) => { + dragStart(e); + interactionMode = "point-touch"; + }} + on:mouseup|preventDefault={pointMouseUp(i)} + on:mouseleave|preventDefault={switchToDragPoint(i)} /> {/each} - - {#if dragging && dragBox && !isMainBoxDrag} + + {#if focusedColors.length > 0} dragStart(e)} + on:mousedown|preventDefault={(e) => dragStart(e)} /> {/if} - {#if pickedColors.length && focusedColors.length > 1} + + {#if interactionMode === "select"} + + {#if selectionIsZ} {/if} - + + + {/if} + {#if interactionMode === "drag"} + + dragEnd(e)} + on:mouseup|preventDefault={(e) => dragEnd(e)} + /> + {/if}
@@ -512,8 +478,4 @@ -webkit-transition: r 0.2s ease-in-out; -moz-transition: r 0.2s ease-in-out; } - - /* svg { - overflow: visible; - } */ diff --git a/src/components/ColorScatterPlotPolarGuide.svelte b/src/components/ColorScatterPlotPolarGuide.svelte index bf5e8bad..56d11782 100644 --- a/src/components/ColorScatterPlotPolarGuide.svelte +++ b/src/components/ColorScatterPlotPolarGuide.svelte @@ -14,7 +14,7 @@ export let colorSpace: string; export let dragging: boolean; export let axisColor: string; - export let textColor: string; + export const textColor: string = ""; $: config = colorPickerConfig[colorSpace as keyof typeof colorPickerConfig]; $: rNonDimScale = scaleLinear().domain([0, 1]).range(rScale.domain()); diff --git a/src/components/ColorScatterPlotZGuide.svelte b/src/components/ColorScatterPlotZGuide.svelte index 9399ca2b..a44cc112 100644 --- a/src/components/ColorScatterPlotZGuide.svelte +++ b/src/components/ColorScatterPlotZGuide.svelte @@ -1,6 +1,6 @@ number; z: (color: Color) => number; } -) { +) => number[]; + +const between = (x: number, a: number, b: number) => a <= x && x <= b; +export const selectColorsFromDragZ: selectColorsFromDrag = ( + dragBox, + dragging, + parentPos, + colors, + config +) => { const { yMin, yMax } = dragExtent(dragBox, dragging, parentPos); + // magic offset required to make the selection box work + // dunno why + const magicOffset = 15; const newFocusedColors = colors - .map((color, idx) => { - const y = config.z(color); - return yMin <= y && y <= yMax ? idx : -1; - }) + .map((color, idx) => + between(config.z(color) + magicOffset, yMin, yMax) ? idx : -1 + ) .filter((x) => x !== -1); return [...newFocusedColors]; - // onFocusedColorsChange([...newFocusedColors]); -} +}; -export function selectColorsFromDrag( - dragBox: { x: number; y: number }, - dragging: { x: number; y: number }, - parentPos: { x: number; y: number }, - colors: Color[], - config: { - isPolar: boolean; - plotHeight: number; - plotWidth: number; - x: (color: Color) => number; - y: (color: Color) => number; - z: (color: Color) => number; - } -) { +export const selectColorsFromDrag: selectColorsFromDrag = ( + dragBox, + dragging, + parentPos, + colors, + config +) => { const { xMin, xMax, yMin, yMax } = dragExtent(dragBox, dragging, parentPos); // check if selected in screen space @@ -260,13 +263,13 @@ export function selectColorsFromDrag( xVal += config.plotWidth / 2; yVal += config.plotHeight / 2; } - const inXBound = xMin <= xVal && xVal <= xMax; - const inYBound = yMin <= yVal && yVal <= yMax; + const inXBound = between(xVal, xMin, xMax); + const inYBound = between(yVal, yMin, yMax); return inXBound && inYBound ? idx : -1; }) .filter((x) => x !== -1); return [...newFocusedColors]; -} +}; export const toXY = (e: any) => { const touches = e?.touches?.length ? e.touches : e?.changedTouches || []; @@ -318,3 +321,66 @@ export function makeScales( const zInv = (z: number) => zScale.invert(z); return { x, y, z, xInv, yInv, zInv }; } + +type dragEventToColor = ( + e: any, + originalColor: Color, + originalPos: { x: number; y: number }, + config: (typeof colorPickerConfig)[string], + scales: ReturnType, + colorSpace: any, + dragDelta: { x: number; y: number } +) => Color; +export const dragEventToColorZ: dragEventToColor = ( + e, + originalColor, + originalPos, + config, + scales, + colorSpace, + dragDelta +) => { + const { zInv, z } = scales; + const screenPosDelta = toXY(e).y - originalPos.y + dragDelta.y; + const coords = originalColor.toChannels(); + const zClamp = (v: number) => clampToRange(v, config.zDomain); + coords[config.zChannelIndex] = zClamp( + zInv(z(originalColor) + screenPosDelta) + ); + + return Color.colorFromChannels(coords, colorSpace); +}; + +export const dragEventToColorXY: dragEventToColor = ( + e, + originalColor, + originalPos, + config, + scales, + colorSpace, + dragDelta +) => { + const { x, y, xInv, yInv } = scales; + const values = toXY(e); + const screenPosDelta = { + x: values.x - originalPos.x + dragDelta.x, + y: values.y - originalPos.y + dragDelta.y, + }; + + const xClamp = (v: number) => clampToRange(v, config.xDomain); + const yClamp = (v: number) => clampToRange(v, config.yDomain); + // screen coordinates + const newPos = [ + x(originalColor) + screenPosDelta.x, + y(originalColor) + screenPosDelta.y, + ]; + // color space coordinates + const newVal = [ + xClamp(xInv(newPos[0], newPos[1])), + yClamp(yInv(newPos[0], newPos[1])), + ]; + const coords = originalColor.toChannels(); + coords[config.xChannelIndex] = newVal[0]; + coords[config.yChannelIndex] = newVal[1]; + return Color.colorFromChannels(coords, colorSpace); +};