From 89c1d44141491fe775418c776e12b09a42e112a5 Mon Sep 17 00:00:00 2001 From: David Mosbach Date: Sat, 26 Aug 2023 03:12:42 +0200 Subject: [PATCH] improved interaction & styling --- editor.ts | 147 +++++++++++++++++++++++++++++++-------------- start.sh | 6 +- webpack.config.cjs | 7 ++- workflow.ts | 5 +- 4 files changed, 111 insertions(+), 54 deletions(-) diff --git a/editor.ts b/editor.ts index 9bf7180..5a0cc01 100644 --- a/editor.ts +++ b/editor.ts @@ -220,9 +220,12 @@ export function search(text: string) { head.classList.add('search-result-head'); r.appendChild(head); var info = document.createElement('div'); - if ((target).hasOwnProperty('actionData')) - info.innerText = (target).source.name + ' → ' + (target).target.name; - else + if ((target).hasOwnProperty('actionData')) { + var eTarget = (target) + var src = (eTarget.source instanceof WF.WFNode) ? eTarget.source.name : '?'; + var tgt = (eTarget.target instanceof WF.WFNode) ? eTarget.target.name : '?'; + info.innerText = src + ' → ' + tgt; + } else info.innerText = (target).stateData.abbreviation; info.setAttribute('title', info.innerText); info.classList.add('search-result-info'); @@ -421,7 +424,7 @@ export function selectViewer() { }); } -var selection : WF.WFNode | WF.WFEdge | null = null; // The currently selected node/edge. +var selection : WF.WFNode | WF.WFGhostNode | WF.WFEdge | null = null; // The currently selected node/edge. var rightSelection : WF.WFNode | WF.WFEdge | null = null; // The currently right clicked node/edge. var edgeTo : WF.WFNode | null = null; // Target of an edge to be created. var edgeFrom : WF.WFNode | null = null; // Start on an edge to be created. @@ -510,15 +513,16 @@ document.querySelectorAll('.delete-item').forEach(elem => elem.addEventListener( * Marks the given item as selected. * @param {*} item The node or edge to select. */ -export function select(item: WF.WFEdge | WF.WFNode) { +export function select(item: WF.WFEdge | WF.WFNode | WF.WFGhostNode) { closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); edgeFrom = edgeTo = rightSelection = null; selection = selection === item ? null : item; - if (selection === item) { + if (!(item instanceof WF.WFGhostNode) && selection === item) { while (sideContent.firstChild) sideContent.removeChild(sideContent.lastChild); function callback() { - if (item.hasOwnProperty('actionData')) { + if (item instanceof WF.WFGhostNode) return; + if (item instanceof WF.WFEdge) { sideInfoEdge.style.display = 'block'; sideInfoNode.style.display = 'none'; } else { @@ -589,20 +593,17 @@ export function addEdge() { id: `@@ghost@(${x},${y})`, x: x, y: y, - name: 'Drag me!', fx: x, fy: y, - val: 5 }); + val: 7 }); var ghostState2 = new WF.WFGhostNode({ - id: `@@ghost@(${x+200},${y})`, - x: x + 200, + id: `@@ghost@(${x+50},${y})`, + x: x + 50, y: y, - name: 'Drag me!', - fx: x + 200, + fx: x + 50, fy: y, - val: 5 }); + val: 7 }); workflow.states.push(ghostState, ghostState2); - console.log('is ghost:', ghostState instanceof WF.WFGhostNode, ghostState2 instanceof WF.WFGhostNode); updateGraph(); connect(ghostState, ghostState2); } @@ -804,7 +805,7 @@ function prepareWorkflow() { //Create search index workflow.states.forEach(state => - nodeIndex.add(state.id, state.name) + (state instanceof WF.WFNode) && nodeIndex.add(state.id, state.name) ); workflow.actions.forEach(action => actionIndex.add(action.id, action.name) @@ -965,30 +966,75 @@ const edgeColourSubtleSelected = '#00000055'; const edgeColourSubtleSelectedDarkMode = '#ffffff55'; const edgeColourMostSubtle = '#99999944'; +class Colour { + + private baseValue: string; + baseDark: string; + + constructor(r: number, g: number, b: number) { + var arr = [r,g,b]; + if (! arr.every((val: number) => val >= 0 && val <= 255)) + throw new Error('rgb out of bounds (0,255)'); + this.baseValue = '#' + arr.map((v: number) => v.toString(16).padStart(2, '0')).join(''); + this.baseDark = '#' + arr.map((v: number) => (Math.max(v-80, 0)).toString(16).padStart(2, '0')).join(''); + Object.seal(this); + console.log('colour:', this.baseValue, 'dark:', this.baseDark); + + } + + value(alpha?: number) { + if (alpha === undefined) return this.baseValue; + if (! (alpha >= 0 && alpha <= 255)) + throw new Error('rgba out of bounds (0,255)'); + return this.baseValue + alpha.toString(16).padStart(2, '0'); + } + + dark(alpha?: number) { + if (alpha === undefined) return this.baseDark; + if (! (alpha >= 0 && alpha <= 255)) + throw new Error('rgba out of bounds (0,255)'); + return this.baseDark + alpha.toString(16).padStart(2, '0'); + } +} + +const nodeColourDefault = new Colour(0x36, 0x79, 0xd2); +const nodeColourSelected = new Colour(0x53, 0x8c, 0xd9); +const nodeColourDefaultUnknown = new Colour(0xee, 0xaa, 0x00); +const nodeColourSelectedUnknown = new Colour(0xff, 0xbc, 0x15); +const nodeColourDefaultFinal = new Colour(0x31, 0xa8, 0x10); +const nodeColourSelectedFinal = new Colour(0x3a, 0xc7, 0x13); +const nodeColourDefaultNotOk = new Colour(0xe7, 0x21, 0x5a); +const nodeColourSelectedNotOk = new Colour(0xec, 0x4e, 0x7b); +const nodeColourDefaultInit = new Colour(0xee, 0xaa, 0x00); +const nodeColourSelectedInit = new Colour(0xff, 0xbc, 0x15); +const nodeColourGhost = new Colour(0xff, 0xff, 0xff); +const nodeColourGhostDark = new Colour(0x00, 0x00, 0x00); + + + + + /** * * @param node * @returns The colour the given node should have. */ -function getNodeColour(node: WF.WFNode | WF.WFGhostNode) { - var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) - || highlightedSources.includes(node.id) || highlightedTargets.includes(node.id) - var alpha = standard ? 'ff' : '55'; +function getNodeColour(node: WF.WFNode | WF.WFGhostNode) : Colour { var isSelected = selection === node || rightSelection === node; if (node instanceof WF.WFNode && node.stateData.final !== 'false' && node.stateData.final !== '') { if (node.stateData.final === 'true' || node.stateData.final === 'ok') { - return (isSelected ? '#3ac713' : '#31a810') + alpha; + return isSelected ? nodeColourSelectedFinal : nodeColourDefaultFinal; } else if (node.stateData.final === 'not-ok') { - return (isSelected ? '#ec4e7b' : '#e7215a') + alpha; + return isSelected ? nodeColourSelectedNotOk : nodeColourDefaultNotOk; } else { - return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; + return isSelected ? nodeColourSelectedUnknown : nodeColourDefaultUnknown; } - } else if (node.name === '@@INIT') { - return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; } else if (node instanceof WF.WFGhostNode) { - return '#cafc03ff'; + return darkMode ? nodeColourGhostDark : nodeColourGhost; + } else if (node.name === '@@INIT') { + return isSelected ? nodeColourSelectedInit : nodeColourDefaultInit; } else { - return (isSelected ? '#538cd9' : '#3679d2') + alpha; + return isSelected ? nodeColourSelected : nodeColourDefault; } } @@ -1114,48 +1160,59 @@ function getEdgeColour(edge: LinkObject) { .linkDirectionalParticleWidth((edge: LinkObject) => (isHighlightedActorEdge(edge as WF.WFEdge)) ? 3 : 0) .nodeCanvasObject((node: NodeObject, ctx: CanvasRenderingContext2D) => { const wfNode = (node instanceof WF.WFNode) ? node as WF.WFNode : node as WF.WFGhostNode; - ctx.fillStyle = getNodeColour(wfNode); + var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) + || highlightedSources.includes(wfNode.id) || highlightedTargets.includes(wfNode.id) + var alpha : number; + if (wfNode instanceof WF.WFGhostNode) alpha = 0x80; + else if (standard) alpha = 0xff; + else alpha = 0x55; + var colour = getNodeColour(wfNode); + ctx.save(); + ctx.fillStyle = colour.value(alpha); + ctx.shadowColor = colour.dark(0x80); + ctx.shadowBlur = 20; ctx.beginPath(); ctx.arc(wfNode.x, wfNode.y, 2*wfNode.val, 0, 2 * Math.PI, false); ctx.fill(); - - var selected = (node === selection || node === rightSelection); - ctx.lineWidth = selected ? 1 : 0.2; + ctx.restore(); if (node instanceof WF.WFGhostNode) { ctx.save() ctx.setLineDash([1, 2]); - ctx.strokeStyle = darkMode ? 'white' : 'black'; + ctx.strokeStyle = nodeColourDefaultNotOk.value(); + ctx.shadowColor = nodeColourDefaultNotOk.dark(0x80); + ctx.shadowBlur = 20; ctx.lineCap = 'round'; + ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); - } else { + } else if (node === selection || node === rightSelection) { ctx.save(); ctx.lineCap = 'round'; - if (selected) - ctx.strokeStyle = darkMode ? 'white' : 'black'; - else - ctx.strokeStyle = !darkMode ? 'white' : 'black'; + ctx.lineWidth = 1; + ctx.strokeStyle = darkMode ? 'white' : 'black'; ctx.stroke(); ctx.restore(); } - ctx.fillStyle = 'white'; ctx.font = '4px Inter'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - if (wfNode instanceof WF.WFNode && wfNode.stateData.abbreviation) + if (wfNode instanceof WF.WFNode && wfNode.stateData.abbreviation) { + ctx.fillStyle = 'white'; ctx.fillText(wfNode.stateData.abbreviation, wfNode.x, wfNode.y); - else if (wfNode instanceof WF.WFGhostNode) + } else if (wfNode instanceof WF.WFGhostNode) { + ctx.fillStyle = darkMode ? 'white' : 'black'; ctx.fillText(wfNode.text, wfNode.x, wfNode.y); + } }) .onNodeDragEnd((node: NodeObject) => { node.fx = node.x; node.fy = node.y; }) .onNodeClick((node: NodeObject, _: MouseEvent) => { - const wfNode = node as WF.WFNode; + const wfNode = node as (WF.WFNode | WF.WFGhostNode); if (edgeFrom) { connect(edgeFrom, wfNode); edgeFrom = null; @@ -1166,6 +1223,10 @@ function getEdgeColour(edge: LinkObject) { closeMenuItem(); }) .onNodeRightClick((node: NodeObject, event: MouseEvent) => { + if (node instanceof WF.WFGhostNode) { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + return; + } //@ts-ignore TODO replace layerX/layerY openContextMenu(event.layerX, event.layerY, contextMenuSt); closeContextMenus(contextMenuBg, contextMenuEd); @@ -1210,21 +1271,19 @@ function getEdgeColour(edge: LinkObject) { (wfGraph.d3Force('charge')).initialize = function(_nodes: NodeObject[], ...args: any) { var nodes : WF.WFNode[] = []; _nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node))); - console.log('rem. total:', nodes.length); //TODO already store them instead of computing each tick + //TODO already store them instead of computing each tick oldChargeInit(nodes, args); }; (wfGraph.d3Force('center')).initialize = function(_nodes: NodeObject[], ...args: any) { var nodes : WF.WFNode[] = []; _nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node))); - console.log('rem. total:', nodes.length); oldCenterInit(nodes, args); }; (wfGraph.d3Force('link')).initialize = function(_nodes: NodeObject[], ...args: any) { var nodes : WF.WFNode[] = []; _nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node))); - console.log('rem. total:', nodes.length); oldLinkInit(nodes, args); }; diff --git a/start.sh b/start.sh index b92e75e..e4d1689 100755 --- a/start.sh +++ b/start.sh @@ -5,9 +5,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -# echo 'Transpiling to JS...' -# npx tsc -echo 'Transpiling to JS & generating Webpack bundle...' +echo 'Transpiling to JS...' +npx tsc +echo 'Generating Webpack bundle...' npx webpack echo 'Starting server...' npx http-server --cors -o ./editor.html diff --git a/webpack.config.cjs b/webpack.config.cjs index 4b0b66f..491316e 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -1,18 +1,19 @@ const path = require('path'); module.exports = { - entry: './editor.ts', + mode: 'development', + entry: './editor.js', module: { rules: [ { test: /\.tsx?$/, - use: 'ts-loader', + use: 'js-loader', exclude: /node_modules/, }, ], }, resolve: { - extensions: ['.tsx', '.ts', '.js'], + extensions: ['.js'], }, output: { filename: 'bundle.js', diff --git a/workflow.ts b/workflow.ts index 49d27cf..124ab42 100644 --- a/workflow.ts +++ b/workflow.ts @@ -26,7 +26,6 @@ export type GhostNodeFormat = { fx: number, fy: number, id: string, - name: string, val: number } @@ -173,17 +172,15 @@ export class WFGhostNode implements NodeObject { fx: number; fy: number; id: string; - name: string; val: number; constructor(json: GhostNodeFormat) { - this.text = 'Drag me!'; + this.text = '?'; this.x = json.x; this.y = json.y; this.fx = json.fx; this.fy = json.fy; this.id = json.id; - this.name = json.name; this.val = json.val; } }