From b78c9ce34a59e1532b83ec41fe67baa309a61edf Mon Sep 17 00:00:00 2001 From: Toby Milner-Gulland <tmgulland@movingbrands.com> Date: Mon, 13 Apr 2020 22:25:41 +0100 Subject: [PATCH] working on eventhandlers for different modes --- .DS_Store | Bin 0 -> 6148 bytes app/package-lock.json | 110 +++++++- app/package.json | 1 + app/src/experimental/PanZoomContainer.vue | 254 ++++++------------ app/src/experimental/geometry.js | 30 --- .../experimental/layers/SelectionLayer.vue | 87 ++++++ .../experimental/makePassiveEventOption.js | 19 -- .../experimental/{modes.js => modes/index.js} | 2 +- app/src/experimental/modes/select.js | 5 + app/src/experimental/uiStore.js | 13 +- app/src/experimental/utils/canvas.js | 4 +- app/src/experimental/utils/dom.js | 5 +- app/src/experimental/utils/nodes.js | 14 +- app/src/experimental/utils/numbers.js | 48 ++++ app/src/experimental/utils/svg.js | 6 +- app/src/experimental/utils/view.js | 134 +++++++++ app/src/views/Sandbox.vue | 34 ++- 17 files changed, 519 insertions(+), 247 deletions(-) create mode 100644 .DS_Store delete mode 100644 app/src/experimental/geometry.js create mode 100644 app/src/experimental/layers/SelectionLayer.vue delete mode 100644 app/src/experimental/makePassiveEventOption.js rename app/src/experimental/{modes.js => modes/index.js} (97%) create mode 100644 app/src/experimental/modes/select.js create mode 100644 app/src/experimental/utils/view.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fadd49af93a07740151b4698fdea46e28fdbf625 GIT binary patch literal 6148 zcmeH~I}XA?3`A{6fkcy%avKi74OR$Fzy<h~kw8N9dvwO1hCq!XG?whQ_QpywMK%@@ z-9E1ykzPa=aHFg(j7*Ww<s^5xUmus@e7g-+a+@Ws0PkhApW6f#paN8Y3Qz$mFd+r< zAYUve^h|sdDnJFMp@4lK3fx$eE$E*P1Rnvw4rMp2eU<==6~LNoK~!KGtzfjOk0Dm~ zcCh4iHQ9pEE}FxK=AG517??)8Xh8zg>R_M(RA8jQJo4Vo|2_QM{6A`8N(HFEpDCc- z?y%e9rSfe3cs;8hvuf)G2mNw{x1Rtcb`-DRZrCrj0Bf=ZQGxMCz-3^d0zXyY1qdw> AZU6uP literal 0 HcmV?d00001 diff --git a/app/package-lock.json b/app/package-lock.json index 3702440..596be55 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "nodenogg.in", - "version": "0.1.10", + "version": "0.1.11", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2060,6 +2060,14 @@ "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", "dev": true }, + "affine-hull": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/affine-hull/-/affine-hull-1.0.0.tgz", + "integrity": "sha1-dj/x040GPOt+Jy8X7k17vK+QXF0=", + "requires": { + "robust-orientation": "^1.1.3" + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -2183,6 +2191,11 @@ "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==", "dev": true }, + "area-polygon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/area-polygon/-/area-polygon-1.0.1.tgz", + "integrity": "sha1-gCp1QIOL5Bi4rhzZnB5Mu7ktbRs=" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2530,6 +2543,11 @@ "file-uri-to-path": "1.0.0" } }, + "bit-twiddle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz", + "integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4=" + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3522,6 +3540,16 @@ } } }, + "convex-hull": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/convex-hull/-/convex-hull-1.0.3.tgz", + "integrity": "sha1-IKOqbOh/St6i/30XlxyfwcZ+H/8=", + "requires": { + "affine-hull": "^1.0.0", + "incremental-convex-hull": "^1.0.1", + "monotone-convex-hull-2d": "^1.0.1" + } + }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", @@ -7031,6 +7059,15 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "incremental-convex-hull": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz", + "integrity": "sha1-UUKMFMudmmFEv+abKFH7N3M0vh4=", + "requires": { + "robust-orientation": "^1.1.2", + "simplicial-complex": "^1.0.0" + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -7171,6 +7208,15 @@ } } }, + "interactive-shape-recognition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/interactive-shape-recognition/-/interactive-shape-recognition-1.0.1.tgz", + "integrity": "sha512-BQZajF3oVJ8l38UbEuFWV2G6kKRvlvZvahiXRY2GN8tE/Ch6iIkKKefPVRbTi///JIwZuy5JcEuxZPEoProhTg==", + "requires": { + "area-polygon": "^1.0.1", + "convex-hull": "^1.0.3" + } + }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -8512,6 +8558,14 @@ } } }, + "monotone-convex-hull-2d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", + "integrity": "sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw=", + "requires": { + "robust-orientation": "^1.1.3" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -10802,6 +10856,36 @@ "inherits": "^2.0.1" } }, + "robust-orientation": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.1.3.tgz", + "integrity": "sha1-2v9bANO+TmByLw6cAVbvln8cIEk=", + "requires": { + "robust-scale": "^1.0.2", + "robust-subtract": "^1.0.0", + "robust-sum": "^1.0.0", + "two-product": "^1.0.2" + } + }, + "robust-scale": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz", + "integrity": "sha1-d1Ey7QlULQKOWLLMecBikLz3jDI=", + "requires": { + "two-product": "^1.0.2", + "two-sum": "^1.0.0" + } + }, + "robust-subtract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz", + "integrity": "sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo=" + }, + "robust-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz", + "integrity": "sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k=" + }, "run-async": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", @@ -11107,6 +11191,15 @@ } } }, + "simplicial-complex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simplicial-complex/-/simplicial-complex-1.0.0.tgz", + "integrity": "sha1-bDOk7Wn81Nkbe8rdOzC2NoPq4kE=", + "requires": { + "bit-twiddle": "^1.0.0", + "union-find": "^1.0.0" + } + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -12108,6 +12201,16 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "two-product": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz", + "integrity": "sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo=" + }, + "two-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz", + "integrity": "sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q=" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -12191,6 +12294,11 @@ "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "dev": true }, + "union-find": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/union-find/-/union-find-1.0.2.tgz", + "integrity": "sha1-KSusQV5q06iVNdI3AQ20pTYoTlg=" + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/app/package.json b/app/package.json index 3111bc2..a6f011d 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "core-js": "^3.6.4", + "interactive-shape-recognition": "^1.0.1", "marked": "^0.8.2", "pouchdb": "^7.2.1", "pouchdb-find": "^7.2.1", diff --git a/app/src/experimental/PanZoomContainer.vue b/app/src/experimental/PanZoomContainer.vue index a5d4be7..adc5007 100644 --- a/app/src/experimental/PanZoomContainer.vue +++ b/app/src/experimental/PanZoomContainer.vue @@ -15,6 +15,7 @@ transform-origin: 0 0; background-size: 40px 40px; background-color: rgb(245, 245, 245); + border: 1px solid rgba(0, 0, 0, 0.25); background-image: radial-gradient( circle, rgba(0, 0, 0, 0.5) 1px, @@ -37,7 +38,7 @@ v-on:wheel.capture="onWheel" v-on:touchstart.passive="onTouchStart" v-on:mousedown="onMouseDown" - v-on:touchmove.passive="onTouchMove" + v-on:touchmove="onTouchMove" v-on:mousemove="onMouseMove" v-on:touchend.passive.capture="onTouchEnd" v-on:mouseup.passive.capture="onMouseUp" @@ -46,26 +47,27 @@ > <div ref="innerContainer" - v-bind:class="{ inner: true, active: pan }" + v-bind:class="{ inner: true, active: true }" v-bind:style="{ width: `${width}px`, height: `${height}px`, transform: `translate(${translation.x}px, ${translation.y}px) scale(${scale})` }" > + {{ JSON.stringify(interaction) }} <slot /> </div> </div> </template> <script> +import { mapState } from 'vuex' +import { constrainTranslation } from '@/experimental/utils/numbers' import { - clamp, - midpoint, - touchPt, - touchDistance, - coordChange -} from './geometry' -// import { mapRange } from '@/experimental/utils/numbers' + getNormalisedInteraction, + changeViewFromWheelEvent, + changeViewFromMultiTouchEvent +} from '@/experimental/utils/view' + export default { name: 'map-interaction', data() { @@ -73,6 +75,11 @@ export default { shouldPreventTouchEndDefault: false } }, + computed: { + ...mapState({ + interaction: state => state.ui.interaction + }) + }, props: { translationBounds: { type: Object, @@ -84,14 +91,6 @@ export default { scale: Number, width: Number, height: Number, - pan: { - type: Boolean, - default: true - }, - zoom: { - type: Boolean, - default: true - }, minScale: { type: Number, default: 0.3 @@ -108,60 +107,63 @@ export default { this.shouldPreventTouchEndDefault = false } - const rect = this.$refs.container.getBoundingClientRect() - - const x = e.clientX - parseInt(rect.left, 10) - const y = e.clientY - parseInt(rect.top, 10) - - const result = { - relative: { x, y }, - canvas: { - x: parseInt((x + -this.translation.x) / this.scale, 10), - y: parseInt((y + -this.translation.y) / this.scale, 10) - } - } - console.log(result) + const { relativePoint, boardPoint } = getNormalisedInteraction( + this.$refs.container, + e, + this.translation, + this.scale + ) + console.log(relativePoint, boardPoint) }, + reset() {}, onMouseDown(e) { e.preventDefault() this.setPointerState([e]) }, - onTouchStart(e) { + onMouseMove(e) { + if (!this.interaction.origin) { + return + } e.preventDefault() - this.setPointerState(e.touches) + this.handleDrag(e) }, onMouseUp() { this.setPointerState([]) }, - onTouchEnd(e) { - this.setPointerState(e.touches) - }, - onMouseMove(e) { - if (!this.startPointerInfo || !this.pan) { - return - } - + onTouchStart(e) { e.preventDefault() - this.onDrag(e) + this.setPointerState(e.touches) }, onTouchMove(e) { - if (!this.startPointerInfo) { + if (!this.interaction.origin) { return } - + console.log('touch') e.preventDefault() const isPinchAction = - e.touches.length == 2 && this.startPointerInfo.pointers.length > 1 + e.touches.length == 2 && this.interaction.origin.points.length > 1 - if (isPinchAction && this.zoom) { - this.scaleFromMultiTouch(e) - } else if (e.touches.length === 1 && this.startPointerInfo && this.pan) { - this.onDrag(e.touches[0]) + if (isPinchAction) { + this.handleMultiTouch(e) + } else if (e.touches.length === 1 && this.interaction.origin) { + this.handleDrag(e.touches[0]) } }, - onDrag(pointer) { - const { translation, pointers } = this.startPointerInfo - const startPointer = pointers[0] + onTouchEnd(e) { + this.setPointerState(e.touches) + }, + onWheel(e) { + e.preventDefault() + e.stopPropagation() + + console.log(e) + + this.handleWheel(e) + }, + + handleDrag(pointer) { + const { translation, points } = this.interaction.origin + const startPointer = points[0] const dragX = pointer.clientX - startPointer.clientX const dragY = pointer.clientY - startPointer.clientY const newTranslation = { @@ -169,155 +171,51 @@ export default { y: translation.y + dragY } + console.log(dragX, dragY) + this.$store.commit( 'ui/setTranslation', - this.clampTranslation(newTranslation) + constrainTranslation(newTranslation, this.translationBounds) ) this.shouldPreventTouchEndDefault = true }, - onWheel(e) { - if (!this.zoom) { + setPointerState(points) { + if (!points || points.length === 0) { + this.$store.commit('ui/resetOrigin') return } - e.preventDefault() - e.stopPropagation() - const scaleChange = 2 ** (e.deltaY * 0.002) - const newScale = clamp( - this.minScale, - this.scale + (1 - scaleChange), - this.maxScale - ) - const mousePos = this.clientPosToTranslatedPos({ - x: e.clientX, - y: e.clientY - }) - this.scaleFromPoint(newScale, mousePos) - }, - setPointerState(pointers) { - if (!pointers || pointers.length === 0) { - this.startPointerInfo = undefined - return - } - - this.startPointerInfo = { - pointers, + this.$store.commit('ui/setOrigin', { + points, scale: this.scale, translation: this.translation - } - }, - clampTranslation(desiredTranslation = this) { - const { x, y } = desiredTranslation - let { xMax, xMin, yMax, yMin } = this.translationBounds - xMin = xMin != undefined ? xMin : -Infinity - yMin = yMin != undefined ? yMin : -Infinity - xMax = xMax != undefined ? xMax : Infinity - yMax = yMax != undefined ? yMax : Infinity - return { - x: clamp(xMin, x, xMax), - y: clamp(yMin, y, yMax) - } - }, - translatedOrigin(translation = this.translation) { - const clientOffset = this.$refs.container.getBoundingClientRect() - return { - x: clientOffset.left + translation.x, - y: clientOffset.top + translation.y - } - }, - clientPosToTranslatedPos({ x, y }, translation = this.translation) { - const origin = this.translatedOrigin(translation) - return { - x: x - origin.x, - y: y - origin.y - } + }) }, - scaleFromPoint(newScale, focalPt) { - const { translation, scale } = this - const scaleRatio = newScale / (scale != 0 ? scale : 1) - const focalPtDelta = { - x: coordChange(focalPt.x, scaleRatio), - y: coordChange(focalPt.y, scaleRatio) - } - const newTranslation = { - x: translation.x - focalPtDelta.x, - y: translation.y - focalPtDelta.y - } + handleWheel(e) { + const [newScale, newTranslation] = changeViewFromWheelEvent( + e, + this.$refs.container, + this + ) this.$store.commit('ui/setScale', newScale) - this.$store.commit( 'ui/setTranslation', - this.clampTranslation(newTranslation) + constrainTranslation(newTranslation, this.translationBounds) ) }, - scaleFromMultiTouch(e) { - const startTouches = this.startPointerInfo.pointers - const newTouches = e.touches - // calculate new scale - const dist0 = touchDistance(startTouches[0], startTouches[1]) - const dist1 = touchDistance(newTouches[0], newTouches[1]) - const scaleChange = dist1 / dist0 - const startScale = this.startPointerInfo.scale - const targetScale = startScale + (scaleChange - 1) * startScale - const newScale = clamp(this.minScale, targetScale, this.maxScale) - // calculate mid points - const newMidPoint = midpoint( - touchPt(newTouches[0]), - touchPt(newTouches[1]) - ) - const startMidpoint = midpoint( - touchPt(startTouches[0]), - touchPt(startTouches[1]) + handleMultiTouch(e) { + const [newTranslation, newScale] = changeViewFromMultiTouchEvent( + this.interaction.origin.points, + e.touches, + this.$refs.container, + this ) - const dragDelta = { - x: newMidPoint.x - startMidpoint.x, - y: newMidPoint.y - startMidpoint.y - } - const scaleRatio = newScale / startScale - const focalPt = this.clientPosToTranslatedPos( - startMidpoint, - this.startPointerInfo.translation - ) - const focalPtDelta = { - x: coordChange(focalPt.x, scaleRatio), - y: coordChange(focalPt.y, scaleRatio) - } - const newTranslation = { - x: this.startPointerInfo.translation.x - focalPtDelta.x + dragDelta.x, - y: this.startPointerInfo.translation.y - focalPtDelta.y + dragDelta.y - } - this.$store.commit('ui/setScale', newScale) - this.$store.commit( 'ui/setTranslation', - this.clampTranslation(newTranslation) + constrainTranslation(newTranslation, this.translationBounds) ) - }, - discreteScaleStepSize() { - const { minScale, maxScale } = this - const delta = Math.abs(maxScale - minScale) - return delta / 10 - }, - changeScale(delta) { - const targetScale = this.scale + delta - const { minScale, maxScale } = this - const scale = clamp(minScale, targetScale, maxScale) - const rect = this.$refs.container.getBoundingClientRect() - const x = rect.left + rect.width / 2 - const y = rect.top + rect.height / 2 - const focalPoint = this.clientPosToTranslatedPos({ - x, - y - }) - this.scaleFromPoint(scale, focalPoint) } - }, - mounted() { - this.containerNode = this.$refs.container - }, - created() { - this.startPointerInfo = undefined } } </script> diff --git a/app/src/experimental/geometry.js b/app/src/experimental/geometry.js deleted file mode 100644 index 45ab1e0..0000000 --- a/app/src/experimental/geometry.js +++ /dev/null @@ -1,30 +0,0 @@ -export const clamp = (min, value, max) => { - return Math.max(min, Math.min(value, max)) -} - -export const distance = (p1, p2) => { - const dx = p1.x - p2.x - const dy = p1.y - p2.y - return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) -} - -export const midpoint = (p1, p2) => { - return { - x: (p1.x + p2.x) / 2, - y: (p1.y + p2.y) / 2 - } -} - -export const touchPt = touch => { - return { x: touch.clientX, y: touch.clientY } -} - -export const touchDistance = (t0, t1) => { - const p0 = touchPt(t0) - const p1 = touchPt(t1) - return distance(p0, p1) -} - -export const coordChange = (coordinate, scaleRatio) => { - return scaleRatio * coordinate - coordinate -} diff --git a/app/src/experimental/layers/SelectionLayer.vue b/app/src/experimental/layers/SelectionLayer.vue new file mode 100644 index 0000000..992cf29 --- /dev/null +++ b/app/src/experimental/layers/SelectionLayer.vue @@ -0,0 +1,87 @@ +<template> + <canvas ref="canvas" v-bind:width="width" v-bind:height="height"></canvas> +</template> + +<script> +import { palette } from '@/experimental/constants/color' + +import { clear } from '@/experimental/utils/canvas' + +export default { + props: { + width: { + type: Number + }, + height: { + type: Number + }, + shape: { + type: Object + } + }, + mounted() { + this.canvas = this.$refs.canvas + this.context = this.canvas.getContext('2d') + this.draw() + }, + data() { + return { + canvas: {}, + context: {}, + color: palette.blue.dark + } + }, + // Watch the props for changes and, if necessary, redraw canvas + watch: { + width() { + this.draw() + }, + height() { + this.draw() + }, + shape() { + this.draw() + }, + selected() { + this.draw() + } + }, + methods: { + /** + * Clears the context, renders selection + **/ + draw() { + clear(this.context, this.canvas.width, this.canvas.height) + this.renderSelection() + }, + + renderSelection() { + this.context.globalAlpha = 0.75 + if (this.shape) { + this.context.globalAlpha = 0.5 + const { x, y, width, height } = this.shape + this.context.fillStyle = this.color + this.context.setTransform(1, 0, 0, 1, x, y) + this.context.beginPath() + + this.context.fillRect(0, 0, width, height) + this.context.closePath() + + this.context.setTransform(1, 0, 0, 1, 0, 0) + } + } + } +} +</script> +<style scoped> +canvas { + z-index: 3; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + /* opacity: 0.5; */ +} +</style> diff --git a/app/src/experimental/makePassiveEventOption.js b/app/src/experimental/makePassiveEventOption.js deleted file mode 100644 index 8b32c88..0000000 --- a/app/src/experimental/makePassiveEventOption.js +++ /dev/null @@ -1,19 +0,0 @@ -let passiveSupported = false -try { - const options = { - /* eslint-disable getter-return */ - get passive() { - passiveSupported = true - } - } - window.addEventListener('test', options, options) - window.removeEventListener('test', options, options) -} catch { - passiveSupported = false -} - -const makePassiveEventOption = passive => { - return passiveSupported ? { passive } : passive -} - -export default makePassiveEventOption diff --git a/app/src/experimental/modes.js b/app/src/experimental/modes/index.js similarity index 97% rename from app/src/experimental/modes.js rename to app/src/experimental/modes/index.js index e87115e..88ccc2c 100644 --- a/app/src/experimental/modes.js +++ b/app/src/experimental/modes/index.js @@ -2,7 +2,7 @@ export const select = { name: 'select', view: { pan: false, - zoom: true + zoom: false }, icon: 'select', cursor: 'initial', diff --git a/app/src/experimental/modes/select.js b/app/src/experimental/modes/select.js new file mode 100644 index 0000000..72ea587 --- /dev/null +++ b/app/src/experimental/modes/select.js @@ -0,0 +1,5 @@ +export default { + mixins: { + + } +} \ No newline at end of file diff --git a/app/src/experimental/uiStore.js b/app/src/experimental/uiStore.js index 0726226..bbf903b 100644 --- a/app/src/experimental/uiStore.js +++ b/app/src/experimental/uiStore.js @@ -6,7 +6,12 @@ const store = { interaction: { active: false, origin: null, - shape: null + shape: { + x: 10, + y: 10, + width: 100, + height: 200 + } }, selection: { links: [], @@ -36,6 +41,12 @@ const store = { } }, mutations: { + setOrigin(state, origin) { + state.interaction.origin = origin + }, + resetOrigin(state) { + state.interaction.origin = null + }, setMode(state, mode) { if (allModes[mode]) { state.mode = mode diff --git a/app/src/experimental/utils/canvas.js b/app/src/experimental/utils/canvas.js index c6dc3d5..48db8b7 100644 --- a/app/src/experimental/utils/canvas.js +++ b/app/src/experimental/utils/canvas.js @@ -1,6 +1,6 @@ import * as win from '@/experimental/constants/window' import { generateLinkHandles } from '@/experimental/utils/nodes' -import { mapRange, distance, angleBetween } from '@/experimental/utils/numbers' +import { mapRange, distanceBetween, angleBetween } from '@/experimental/utils/numbers' ////////////////////////////////////////////////////////////////////// // CANVAS UTILITIES @@ -23,7 +23,7 @@ export const drawLinkBezierCurve = (context, from, to, tension) => { // This is a simple way to adjust the link tension depending // on the distance the link covers const adjustedTension = mapRange( - distance(fromHandle, toHandle), + distanceBetween(fromHandle, toHandle), 0, win.width, tension * 0.01, diff --git a/app/src/experimental/utils/dom.js b/app/src/experimental/utils/dom.js index f74ad77..ed19558 100644 --- a/app/src/experimental/utils/dom.js +++ b/app/src/experimental/utils/dom.js @@ -5,7 +5,6 @@ import { isFunction } from '@/experimental/utils/helpers' ////////////////////////////////////////////////////////////////////// /** - * !! INCOMPLETE !! * Normalises mouse/touch interaction events * * @param {HTMLElement} target - base element for event @@ -19,11 +18,11 @@ export const getInteractionPoint = (target, event) => { return { x: parseInt( ((event.clientX - rect.left) / (rect.right - rect.left)) * - target.offsetWidth + target.offsetWidth ), y: parseInt( ((event.clientY - rect.top) / (rect.bottom - rect.top)) * - target.offsetHeight + target.offsetHeight ) } } diff --git a/app/src/experimental/utils/nodes.js b/app/src/experimental/utils/nodes.js index 11ff05b..4e09101 100644 --- a/app/src/experimental/utils/nodes.js +++ b/app/src/experimental/utils/nodes.js @@ -186,12 +186,16 @@ export const generateAreaFromNodes = nodes => { /** * Generates a node shape from two given points + * + * @param {Point} origin + * @param {Point} target + * @return {Node} * */ -export const generateNode = (origin, point) => { - const minusX = origin.x > point.x - const minusY = origin.y > point.y - const width = Math.abs(point.x - origin.x) - const height = Math.abs(point.y - origin.y) +export const generateNode = (origin, target) => { + const minusX = origin.x > target.x + const minusY = origin.y > target.y + const width = Math.abs(target.x - origin.x) + const height = Math.abs(target.y - origin.y) return { x: minusX ? origin.x - width : origin.x, diff --git a/app/src/experimental/utils/numbers.js b/app/src/experimental/utils/numbers.js index 2f5843d..cdb7134 100644 --- a/app/src/experimental/utils/numbers.js +++ b/app/src/experimental/utils/numbers.js @@ -63,3 +63,51 @@ export const mapRange = (value, from1, to1, from2, to2) => * */ export const angleBetween = (point1, point2) => Math.atan2(point2.y - point1.y, point2.x - point1.x) + +/** +* Constrains a value within a range +* +* @param {number} value +* @param {number} min +* @param {number} max +* @return {number} +* +* */ +export const clamp = (value, min, max) => { + return Math.max(min, Math.min(value, max)) +} + +/** + * ! TODO ! confusing naming +* Scales a coordinate value +*/ +export const scaleCoordinate = (coordinate, scaleRatio) => { + return scaleRatio * coordinate - coordinate +} + +/** + * ! TODO ! confusing naming +* Scales a @Point +*/ +export const scalePoint = ({ x, y }, scale) => { + return { + x: scaleCoordinate(x, scale), + y: scaleCoordinate(y, scale) + } +} + +/** + * ! TODO ! dynamic contraints based on width/height of board + */ +export const constrainTranslation = (targetTranslation, translationBounds) => { + const { x, y } = targetTranslation + let { xMax, xMin, yMax, yMin } = translationBounds + xMin = xMin != undefined ? xMin : -Infinity + yMin = yMin != undefined ? yMin : -Infinity + xMax = xMax != undefined ? xMax : Infinity + yMax = yMax != undefined ? yMax : Infinity + return { + x: clamp(x, xMin, xMax), + y: clamp(y, yMin, yMax) + } +} \ No newline at end of file diff --git a/app/src/experimental/utils/svg.js b/app/src/experimental/utils/svg.js index dcf070c..b93e8cd 100644 --- a/app/src/experimental/utils/svg.js +++ b/app/src/experimental/utils/svg.js @@ -1,6 +1,6 @@ import * as win from '@/experimental/constants/window' import { generateLinkHandles } from '@/experimental/utils/nodes' -import { mapRange, distance } from '@/experimental/utils/numbers' +import { mapRange, distanceBetween } from '@/experimental/utils/numbers' export const generateBezierCurve = (from, to, tension) => { if (from && to) { @@ -9,7 +9,7 @@ export const generateBezierCurve = (from, to, tension) => { // This is a simple way to adjust the link tension depending // on the distance the link covers const adjustedTension = mapRange( - distance(fromHandle, toHandle), + distanceBetween(fromHandle, toHandle), 0, win.width, tension * 0.01, @@ -27,7 +27,7 @@ export const makeBezier = (fromHandle, toHandle, tension) => { // This is a simple way to adjust the link tension depending // on the distance the link covers const adjustedTension = mapRange( - distance(fromHandle, toHandle), + distanceBetween(fromHandle, toHandle), 0, win.width, tension * 0.01, diff --git a/app/src/experimental/utils/view.js b/app/src/experimental/utils/view.js new file mode 100644 index 0000000..96d4628 --- /dev/null +++ b/app/src/experimental/utils/view.js @@ -0,0 +1,134 @@ +import { clamp, lerpPoint, scalePoint, distanceBetween } from '@/experimental/utils/numbers' + +////////////////////////////////////////////////////////////////////// +// DOM UTILITIES +////////////////////////////////////////////////////////////////////// + +/** + * Returns position of interaction event both for the containing element + * and the adjusted board position beneath + * + * @param {HTMLElement} target - base element for event + * @param {Event} event + * @param {Object} translation translated board position in {x,y} + * @param {Number} scale board scale + * @returns {Object} + * + * */ +export const getNormalisedInteraction = (target, event, translation, scale) => { + const rect = target.getBoundingClientRect() + const x = event.clientX - parseInt(rect.left, 10) + const y = event.clientY - parseInt(rect.top, 10) + + return { + relativePoint: { x, y }, + boardPoint: containerToBoardPoint({ x, y }, translation, scale) + } +} + +export const containerToBoardPoint = (containerPoint, translation, scale) => { + return { + x: parseInt((containerPoint.x + -translation.x) / scale, 10), + y: parseInt((containerPoint.y + -translation.y) / scale, 10) + } +} + +// ! TODO ! +// export const boardToContainerPoint = (boardPoint, translation, scale) => { +// } + + +/** + * @param {HTMLElement} target - base element for event + * @param {Point} point point to transform + * @param {Object} translation translated board position in {x,y} + * @returns {Point} + * + * */ +export const getTranslatedPositionFromContainer = (target, point, translation) => { + const rect = target.getBoundingClientRect() + + return { + x: point.x - (rect.left + translation.x), + y: point.y - (rect.top + translation.y) + } +} + +export const changeViewFromWheelEvent = (event, container, { translation, scale, minScale, maxScale }) => { + const scaleChange = 2 ** (event.deltaY * 0.002) + const newScale = clamp( + scale + (1 - scaleChange), + minScale, + maxScale + ) + const mousePosition = getTranslatedPositionFromContainer( + container, + { x: event.clientX, y: event.clientY }, + translation + ) + + const scaleRatio = newScale / (scale != 0 ? scale : 1) + const focalPtDelta = scalePoint(mousePosition, scaleRatio) + const newTranslation = { + x: translation.x - focalPtDelta.x, + y: translation.y - focalPtDelta.y + } + + return [newScale, newTranslation] +} + +export const changeViewFromMultiTouch = (startTouches, newTouches, container, { + minScale, + maxScale, + interaction +}) => { + const dist0 = distanceBetween(startTouches[0], startTouches[1]) + const dist1 = distanceBetween(newTouches[0], newTouches[1]) + const scaleChange = dist1 / dist0 + const startScale = interaction.origin.scale + const targetScale = startScale + (scaleChange - 1) * startScale + const newScale = clamp(targetScale, minScale, maxScale) + + // calculate mid points + const newMidPoint = lerpPoint( + touchPoint(newTouches[0]), + touchPoint(newTouches[1]), + 0.5 + ) + const startMidpoint = lerpPoint( + touchPoint(startTouches[0]), + touchPoint(startTouches[1]), + 0.5 + ) + const dragDelta = { + x: newMidPoint.x - startMidpoint.x, + y: newMidPoint.y - startMidpoint.y + } + const scaleRatio = newScale / startScale + const focalPoint = getTranslatedPositionFromContainer( + container, + startMidpoint, + interaction.origin.pointer.translation + ) + const focalPointDelta = scalePoint(focalPoint, scaleRatio) + + const newTranslation = { + x: interaction.origin.pointer.translation.x - focalPointDelta.x + dragDelta.x, + y: interaction.origin.pointer.translation.y - focalPointDelta.y + dragDelta.y + } + return [newTranslation, newScale] +} + +/** + * Normalises touch event position + * + * @param {Event} event + * @returns {Point} + * + * */ +export const touchPoint = touch => { + return { + x: touch.clientX, + y: touch.clientY + } +} \ No newline at end of file diff --git a/app/src/views/Sandbox.vue b/app/src/views/Sandbox.vue index b283c54..d656461 100644 --- a/app/src/views/Sandbox.vue +++ b/app/src/views/Sandbox.vue @@ -1,15 +1,19 @@ <template> - <div class="wrapper" v-bind:style="modeContainerStyle"> + <div ref="container" class="wrapper" v-bind:style="modeContainerStyle"> <PanZoomContainer v-bind:width="width" v-bind:height="height" v-bind:scale="scale" v-bind:translation="translation" - v-bind:pan="activeMode.view.pan" - v-bind:zoom="activeMode.view.zoom" > <h1>Nodes</h1> </PanZoomContainer> + <SelectionLayer + v-if="domContainerReady" + v-bind:shape="interaction.shape" + v-bind:width="elementWidth" + v-bind:height="elementHeight" + /> <ModeToolbar /> <ViewToolbar /> </div> @@ -19,18 +23,25 @@ import PanZoomContainer from '@/experimental/PanZoomContainer' import ModeToolbar from '@/experimental/ModeToolbar' import ViewToolbar from '@/experimental/ViewToolbar' +import SelectionLayer from '@/experimental/layers/SelectionLayer' import { mapGetters, mapState } from 'vuex' export default { name: 'Sandbox', data: function() { return { + elementWidth: undefined, + elementHeight: undefined, width: 2000, height: 2000 } }, computed: { + domContainerReady() { + return !!this.elementWidth && !!this.elementHeight + }, ...mapState({ + interaction: state => state.ui.interaction, scale: state => state.ui.scale, translation: state => state.ui.translation }), @@ -39,10 +50,25 @@ export default { modeContainerStyle: 'ui/modeContainerStyle' }) }, + mounted() { + window.addEventListener('resize', this.handleResize) + this.handleResize() + }, + destroyed() { + window.removeEventListener('resize', this.handleResize) + }, + methods: { + handleResize() { + const { offsetWidth, offsetHeight } = this.$refs.container + this.elementWidth = offsetWidth + this.elementHeight = offsetHeight + } + }, components: { ModeToolbar, ViewToolbar, - PanZoomContainer + PanZoomContainer, + SelectionLayer } } </script> -- GitLab