-
{{ zoom.toFixed(1) }}x
-
{{ (rotate * (180 / Math.PI)).toFixed(0) }}°
+
+
{{ viewStore.zoom.toFixed(1) }}×
+
+ {{ (viewStore.rotate * (180 / Math.PI)).toFixed(0) }}°
+
diff --git a/apps/scishop/src/components/Toolbar.vue b/apps/scishop/src/components/Toolbar.vue
index 754d8fb..916cec1 100644
--- a/apps/scishop/src/components/Toolbar.vue
+++ b/apps/scishop/src/components/Toolbar.vue
@@ -1,5 +1,5 @@
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index 01999c3..097580f 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,4 +1,4 @@
-import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
+import { Ref, watch, ref, unref, shallowRef, triggerRef, computed } from 'vue';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
import { createDitherFilter, renderPixelData } from '@4bitlabs/image';
@@ -9,7 +9,9 @@ import {
Palettes,
} from '@4bitlabs/color';
import { nearestNeighbor } from '@4bitlabs/resize-filters';
-import { get2dContext } from '../helpers/getContext.ts';
+import { get2dContext } from '../helpers/getContext';
+
+const oversampleRef = ref<[number, number]>([5, 5]);
export function useCanvasRenderer(
picDataRef: Ref,
@@ -24,11 +26,11 @@ export function useCanvasRenderer(
});
watch(
- [renderedRef, resRef],
- ([pic, [width, height]]) => {
+ [renderedRef, resRef, oversampleRef],
+ ([pic, [width, height], oversample]) => {
const canvas = unref(canvasRef);
- canvas.width = width * 5;
- canvas.height = height * 6;
+ canvas.width = width * oversample[0];
+ canvas.height = height * oversample[1];
const imgData = renderPixelData(pic.visible, {
dither: createDitherFilter(
@@ -38,8 +40,7 @@ export function useCanvasRenderer(
),
[1, 1],
),
- post: [nearestNeighbor([5, 6])],
- // post: [nearestNeighbor([5, 6])],
+ post: [nearestNeighbor(oversample)],
}) as ImageData;
const ctx = get2dContext(canvas);
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index 0b6b954..771cf7b 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -9,7 +9,14 @@ import {
translate,
} from 'transformation-matrix';
-import store from '../data/store.ts';
+import toolbarStore from '../data/toolbarStore';
+import viewStore from '../data/viewStore';
+
+const clampZoom = (current: number, next: number, min: number, max: number) => {
+ if (current * next < min) return min / current;
+ if (current * next > max) return max / current;
+ return next;
+};
export function useInputMachine(
matrixRef: Ref,
@@ -22,7 +29,7 @@ export function useInputMachine(
if (!el) return;
el.addEventListener('click', (e) => {
- if (store.selectedTool === 'line') {
+ if (toolbarStore.selectedTool === 'line') {
const iMatrix = unref(iMatrixRef);
const screenPos: [number, number] = [e.offsetX, e.offsetY];
const [x, y] = applyToPoint(iMatrix, screenPos);
@@ -45,13 +52,18 @@ export function useInputMachine(
if (!stage) return;
const { width: sWidth, height: sHeight } = stage;
const [dx, dy] = [e.offsetX - sWidth / 2, e.offsetY - sHeight / 2];
-
- store.viewMatrix.value = compose(
+ const scaleFactor = clampZoom(
+ viewStore.zoom,
+ e.deltaY / 1000 + 1,
+ 0.5,
+ 100,
+ );
+ viewStore.viewMatrix = compose(
translate(dx, dy),
- scale(Math.max(0, e.deltaY / 1000 + 1)),
+ scale(scaleFactor),
rotateDEG(-e.deltaX / 40),
translate(-dx, -dy),
- store.viewMatrix.value,
+ viewStore.viewMatrix,
);
});
@@ -59,7 +71,7 @@ export function useInputMachine(
el.addEventListener('pointerdown', (e) => {
if (e.button === 1) {
el.style.cursor = 'grabbing';
- dragState = [store.viewMatrix.value, e.offsetX, e.offsetY];
+ dragState = [viewStore.viewMatrix, e.offsetX, e.offsetY];
}
});
@@ -68,7 +80,7 @@ export function useInputMachine(
const [matrix, ix, iy] = dragState;
const dx = ix - e.offsetX;
const dy = iy - e.offsetY;
- store.viewMatrix.value = compose(translate(-dx, -dy), matrix);
+ viewStore.viewMatrix = compose(translate(-dx, -dy), matrix);
});
el.addEventListener('pointerup', () => {
diff --git a/apps/scishop/src/composables/useResizeWatcher.ts b/apps/scishop/src/composables/useResizeWatcher.ts
index a74f49a..85b3862 100644
--- a/apps/scishop/src/composables/useResizeWatcher.ts
+++ b/apps/scishop/src/composables/useResizeWatcher.ts
@@ -1,6 +1,6 @@
import { watch, onUnmounted, triggerRef, ShallowRef } from 'vue';
-import { useDebouncedRef } from './useDebouncedRef.ts';
+import { useDebouncedRef } from './useDebouncedRef';
export function useResizeWatcher(
refEl: ShallowRef,
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
new file mode 100644
index 0000000..01bf5a8
--- /dev/null
+++ b/apps/scishop/src/data/picStore.ts
@@ -0,0 +1,24 @@
+import { ref, shallowRef, unref } from 'vue';
+
+import { DrawCommand } from '@4bitlabs/sci0';
+
+const data: DrawCommand[] = [];
+
+const layersRef = shallowRef(data);
+const selectedCommandIdx = ref(null);
+const topIdxRef = ref(data.length - 1);
+
+export default {
+ get layers() {
+ return unref(layersRef);
+ },
+ get cmdIdx() {
+ return unref(selectedCommandIdx);
+ },
+ get topIdx() {
+ return unref(topIdxRef);
+ },
+ set topIdx(n: number) {
+ topIdxRef.value = n;
+ },
+};
diff --git a/apps/scishop/src/data/stageStore.ts b/apps/scishop/src/data/stageStore.ts
new file mode 100644
index 0000000..ea7a5c2
--- /dev/null
+++ b/apps/scishop/src/data/stageStore.ts
@@ -0,0 +1,16 @@
+import { ref, unref } from 'vue';
+
+const canvasSizeRef = ref<[number, number]>([320, 190]);
+const aspectRatioRef = ref(6 / 5);
+
+export default {
+ get canvasRes() {
+ return canvasSizeRef;
+ },
+ get aspectRatio() {
+ return unref(aspectRatioRef);
+ },
+ set aspectRatio(value: number) {
+ aspectRatioRef.value = value;
+ },
+};
diff --git a/apps/scishop/src/data/store.ts b/apps/scishop/src/data/store.ts
deleted file mode 100644
index ca963ba..0000000
--- a/apps/scishop/src/data/store.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ref, shallowRef, unref } from 'vue';
-import { identity, Matrix } from 'transformation-matrix';
-
-import { DrawCommand } from '@4bitlabs/sci0';
-
-export type Tool =
- | 'select'
- | 'pan'
- | 'line'
- | 'bezier'
- | 'rect'
- | 'oval'
- | 'brush'
- | 'fill';
-
-const selectedToolRef = ref('pan');
-const selectedCommandIdx = ref(null);
-const cmdsRef = shallowRef([]);
-const topIdxRef = ref(data.length - 1);
-const canvasSizeRef = ref<[number, number]>([320, 190]);
-const aspectRatioRef = ref(6 / 5);
-
-const viewMatrixRef = shallowRef(identity());
-
-export default {
- get selectedTool() {
- return unref(selectedToolRef);
- },
- set selectedTool(next: Tool) {
- selectedToolRef.value = next;
- },
- get canvasRes() {
- return canvasSizeRef;
- },
- get viewMatrix() {
- return viewMatrixRef;
- },
- get cmds() {
- return cmdsRef;
- },
- get cmdIdx() {
- return unref(selectedCommandIdx);
- },
- get topIdx() {
- return unref(topIdxRef);
- },
- set topIdx(n: number) {
- topIdxRef.value = n;
- },
- get aspectRatio() {
- return unref(aspectRatioRef);
- },
- set aspectRatio(value: number) {
- aspectRatioRef.value = value;
- },
-};
diff --git a/apps/scishop/src/data/toolbarStore.ts b/apps/scishop/src/data/toolbarStore.ts
new file mode 100644
index 0000000..963b801
--- /dev/null
+++ b/apps/scishop/src/data/toolbarStore.ts
@@ -0,0 +1,22 @@
+import { ref, unref } from 'vue';
+
+export type Tool =
+ | 'select'
+ | 'pan'
+ | 'line'
+ | 'bezier'
+ | 'rect'
+ | 'oval'
+ | 'brush'
+ | 'fill';
+
+const selectedToolRef = ref('pan');
+
+export default {
+ get selectedTool() {
+ return unref(selectedToolRef);
+ },
+ set selectedTool(next: Tool) {
+ selectedToolRef.value = next;
+ },
+};
diff --git a/apps/scishop/src/data/viewStore.ts b/apps/scishop/src/data/viewStore.ts
new file mode 100644
index 0000000..7f68173
--- /dev/null
+++ b/apps/scishop/src/data/viewStore.ts
@@ -0,0 +1,26 @@
+import { computed, shallowRef, unref } from 'vue';
+import { decomposeTSR, identity, Matrix } from 'transformation-matrix';
+
+const viewMatrixRef = shallowRef(identity());
+const transformRef = computed(() => decomposeTSR(unref(viewMatrixRef)));
+const zoomRef = computed(() => {
+ const { sx, sy } = unref(transformRef).scale;
+ return (sx + sy) / 2;
+});
+const rotateRef = computed(() => unref(transformRef).rotation.angle);
+
+export default {
+ get viewMatrix() {
+ return unref(viewMatrixRef);
+ },
+ set viewMatrix(value: Matrix) {
+ viewMatrixRef.value = value;
+ },
+ get zoom() {
+ return unref(zoomRef);
+ },
+
+ get rotate() {
+ return unref(rotateRef);
+ },
+};
diff --git a/apps/scishop/src/helpers/getPals.ts b/apps/scishop/src/helpers/getPals.ts
index 7666652..4fa6bdd 100644
--- a/apps/scishop/src/helpers/getPals.ts
+++ b/apps/scishop/src/helpers/getPals.ts
@@ -1,7 +1,7 @@
import { DrawCommand } from '@4bitlabs/sci0';
// prettier-ignore
-const DEFAULT_PALETTE: number[] = [
+export const DEFAULT_PALETTE: number[] = [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x88, 0x88, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x88, 0x88, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd,
diff --git a/apps/scishop/src/style.css b/apps/scishop/src/style.css
index d990e29..8804fd8 100644
--- a/apps/scishop/src/style.css
+++ b/apps/scishop/src/style.css
@@ -42,7 +42,7 @@
--clr-surface--default: #f5f5f5;
--clr-surface--shade: color-mix(in srgb, var(--clr-ink), var(--clr-surface--default) 85%);
- --clr-surface--dark: color-mix(in srgb, var(--clr-ink), var(--clr-surface--default) 70%);
+ --clr-surface--glass: #2f2f2f;
--radius: 0.25rlh;
--shadow: 0 0.5rlh 0.5rlh rgba(0 0 0 / 20%);
From 9090a5cb9de8bd7ca6ea674f5341b0b76085df94 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Sat, 25 May 2024 20:36:59 -0600
Subject: [PATCH 07/28] WIP more work on line/user-input.
---
.../scishop/src/components/LayerNavigator.vue | 1 +
apps/scishop/src/components/Sidebar.vue | 7 +-
apps/scishop/src/components/Stage.vue | 65 +++--
apps/scishop/src/components/Toolbar.vue | 3 +-
.../src/composables/useCanvasRenderer.ts | 8 +-
.../src/composables/useInputMachine.ts | 231 ++++++++++++++----
apps/scishop/src/composables/useRafRef.ts | 21 ++
apps/scishop/src/data/picStore.ts | 22 ++
apps/scishop/src/helpers/array-helpers.ts | 10 +
apps/scishop/src/helpers/polygons.ts | 36 +++
apps/scishop/src/helpers/smoothstep.ts | 20 ++
apps/scishop/src/helpers/vec2-helpers.ts | 7 +
12 files changed, 365 insertions(+), 66 deletions(-)
create mode 100644 apps/scishop/src/composables/useRafRef.ts
create mode 100644 apps/scishop/src/helpers/array-helpers.ts
create mode 100644 apps/scishop/src/helpers/polygons.ts
create mode 100644 apps/scishop/src/helpers/smoothstep.ts
create mode 100644 apps/scishop/src/helpers/vec2-helpers.ts
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index 41f6d03..1333185 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -29,6 +29,7 @@ const itemType = {
-
-
+
+
@@ -20,7 +20,4 @@ import LayerNavigator from './LayerNavigator.vue';
gap: 3px;
border-left: 3px solid var(--clr-surface--default);
}
-
-.panel {
-}
diff --git a/apps/scishop/src/components/Stage.vue b/apps/scishop/src/components/Stage.vue
index 6513e54..8136385 100644
--- a/apps/scishop/src/components/Stage.vue
+++ b/apps/scishop/src/components/Stage.vue
@@ -1,35 +1,46 @@
-
+
+
diff --git a/apps/scishop/src/components/Toolbar.vue b/apps/scishop/src/components/Toolbar.vue
index 916cec1..8713a06 100644
--- a/apps/scishop/src/components/Toolbar.vue
+++ b/apps/scishop/src/components/Toolbar.vue
@@ -114,6 +114,7 @@ import store from '../data/toolbarStore';
background-clip: padding-box;
letter-spacing: -0.03ex;
transition: box-shadow 500ms;
+ z-index: 1;
&:first-child {
border-top-left-radius: var(--radius);
@@ -130,7 +131,7 @@ import store from '../data/toolbarStore';
color: var(--clr-surface--default);
font-weight: 700;
box-shadow: 0 0 4rem -0.25rem var(--clr-primary-A90);
- z-index: 1;
+ z-index: 2;
}
}
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index 097580f..baa1049 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,4 +1,4 @@
-import { Ref, watch, ref, unref, shallowRef, triggerRef, computed } from 'vue';
+import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
import { createDitherFilter, renderPixelData } from '@4bitlabs/image';
@@ -10,8 +10,12 @@ import {
} from '@4bitlabs/color';
import { nearestNeighbor } from '@4bitlabs/resize-filters';
import { get2dContext } from '../helpers/getContext';
+import viewStore from '../data/viewStore.ts';
-const oversampleRef = ref<[number, number]>([5, 5]);
+const oversampleRef = computed<[number, number]>(() => {
+ const samples = Math.min(Math.max(1, Math.ceil(viewStore.zoom)), 5);
+ return [samples, samples];
+});
export function useCanvasRenderer(
picDataRef: Ref
,
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index 771cf7b..a4bedb7 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -1,6 +1,16 @@
-import { computed, onMounted, unref, Ref } from 'vue';
+import {
+ Ref,
+ computed,
+ onMounted,
+ ref,
+ shallowRef,
+ unref,
+ watch,
+ watchEffect,
+} from 'vue';
import {
applyToPoint,
+ applyToPoints,
compose,
inverse,
Matrix,
@@ -11,6 +21,17 @@ import {
import toolbarStore from '../data/toolbarStore';
import viewStore from '../data/viewStore';
+import { get2dContext } from '../helpers/getContext.ts';
+import {
+ isInsideBounds,
+ pixel,
+ areaOfPolygon,
+ pathPoly,
+} from '../helpers/polygons.ts';
+import * as SmoothStep from '../helpers/smoothstep';
+import { useRafRef } from './useRafRef.ts';
+import { currentCommandStore as cmdStore } from '../data/picStore.ts';
+import { intVec2 } from '../helpers/vec2-helpers.ts';
const clampZoom = (current: number, next: number, min: number, max: number) => {
if (current * next < min) return min / current;
@@ -18,39 +39,158 @@ const clampZoom = (current: number, next: number, min: number, max: number) => {
return next;
};
+type CSSCursor = string;
+
export function useInputMachine(
matrixRef: Ref,
stageRef: Ref,
+ stageResRef: Ref<[number, number]>,
+ canvasResRef: Ref<[number, number]>,
) {
const iMatrixRef = computed(() => inverse(unref(matrixRef)));
+ const dragStateRef = shallowRef<[Matrix, number, number] | null>(null);
+ const currentCursorRef = ref('auto');
+ const cursorPositionRef = useRafRef<[number, number]>([0, 0]);
- onMounted(() => {
+ const projected = computed((prev) => {
+ const sPos = unref(cursorPositionRef);
+ const next = intVec2(applyToPoint(unref(iMatrixRef), sPos));
+ const isSame = prev && prev[0] === next[0] && prev[1] === next[1];
+ return isSame ? prev : next;
+ });
+
+ watchEffect(() => {
const el = unref(stageRef);
if (!el) return;
+ el.style.cursor = currentCursorRef.value;
+ });
+
+ watch([cursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
+ if (!dragState) return;
+
+ const [matrix, ix, iy] = dragState;
+ const dx = ix - cX;
+ const dy = iy - cY;
+ viewStore.viewMatrix = compose(translate(-dx, -dy), matrix);
+ });
+
+ watch(
+ [stageRef, matrixRef, iMatrixRef, cursorPositionRef],
+ ([el, matrix, iMatrix, screenPoint]) => {
+ if (!el) return;
+
+ const [sWidth, sHeight] = unref(stageResRef);
+ el.width = sWidth;
+ el.height = sHeight;
+ const ctx = get2dContext(el);
+ ctx.clearRect(0, 0, sWidth, sHeight);
- el.addEventListener('click', (e) => {
if (toolbarStore.selectedTool === 'line') {
- const iMatrix = unref(iMatrixRef);
- const screenPos: [number, number] = [e.offsetX, e.offsetY];
- const [x, y] = applyToPoint(iMatrix, screenPos);
- console.log('line:', x, y);
- // const cmd = unref(store.selectedCmd);
- // if (cmd[0] === 'PLINE') {
- // store.cmds.value.splice(
- // store.cmds.value.length - store.cmdIdx - 1,
- // 1,
- // [...cmd, [Math.floor(x), Math.floor(y)]],
- // );
- // triggerRef(store.cmds);
- // console.log('appending');
- // }
+ const canvasPoint = applyToPoint(iMatrix, screenPoint);
+
+ const [cWidth, cHeight] = unref(canvasResRef);
+
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'white';
+ const overCanvas = isInsideBounds([cWidth, cHeight], canvasPoint);
+ currentCursorRef.value = overCanvas ? 'none' : 'auto';
+ if (overCanvas) {
+ const pixelBounds = applyToPoints(matrix, pixel(canvasPoint, 0.0125));
+
+ const area = areaOfPolygon(pixelBounds);
+ const alpha = SmoothStep.s1(1, 6 * 6, area);
+ ctx.save();
+ pathPoly(ctx, pixelBounds);
+ ctx.setLineDash([1, 1]);
+ ctx.strokeStyle = `oklab(${(alpha * 100).toFixed(0)}% 0 0)`;
+ ctx.fillStyle = `oklab(${(alpha * 100 * 0.45).toFixed(0)}% 0 0)`;
+ ctx.fill();
+ ctx.stroke();
+ ctx.restore();
+
+ ctx.fillStyle = 'white';
+ ctx.beginPath();
+ ctx.arc(...screenPoint, 2, 0, Math.PI * 2);
+ ctx.fill();
+ }
}
- });
+ },
+ );
+
+ watch([projected], ([pos]) => {
+ const current = cmdStore.current;
+ if (current === null) return;
+ if (current[0] === 'PLINE') {
+ const [id, mode, code, ...coords] = current;
+ cmdStore.current = [id, mode, code, ...coords.slice(0, -1), pos];
+ }
+ });
- el.addEventListener('wheel', (e) => {
+ watchEffect(() => {
+ const el = unref(stageRef);
+ if (!el) return;
+
+ const stateState = unref(dragStateRef);
+ const { selectedTool } = toolbarStore;
+
+ if (stateState !== null) {
+ currentCursorRef.value = 'grabbing';
+ } else if (selectedTool === 'pan') {
+ currentCursorRef.value = 'grab';
+ } else if (selectedTool === 'select') {
+ currentCursorRef.value = 'crosshair';
+ } else {
+ currentCursorRef.value = 'auto';
+ }
+ });
+
+ const mouseHandlers = {
+ click: (e: MouseEvent) => {
+ console.log(e.button);
+ if (toolbarStore.selectedTool === 'line') {
+ const [x, y] = applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]);
+ const pos: readonly [number, number] = [Math.floor(x), Math.floor(y)];
+
+ const current = cmdStore.current;
+ if (current === null) {
+ cmdStore.current = [
+ 'PLINE',
+ 1,
+ [Math.floor(Math.random() * 40), 0, 0],
+ pos,
+ pos,
+ ];
+ } else if (current[0] === 'PLINE') {
+ const [id, mode, code, ...coords] = current;
+ cmdStore.current = [id, mode, code, ...coords.slice(0, -1), pos, pos];
+ } else {
+ throw new Error(`can't append to existing command ${current[0]}`);
+ }
+ }
+ },
+
+ contextMenu: (e: MouseEvent) => {
+ if (toolbarStore.selectedTool === 'line') {
+ e.preventDefault();
+ const current = cmdStore.current;
+ if (current === null || current[0] !== 'PLINE') return;
+
+ const [id, mode, code, ...coords] = current;
+ if (coords.length < 3) {
+ cmdStore.abort();
+ } else {
+ cmdStore.current = [id, mode, code, ...coords.slice(0, -1)];
+ cmdStore.commit();
+ }
+
+ return;
+ }
+ },
+
+ wheel: (e: WheelEvent) => {
const stage = unref(stageRef);
if (!stage) return;
- const { width: sWidth, height: sHeight } = stage;
+ const [sWidth, sHeight] = unref(stageResRef);
const [dx, dy] = [e.offsetX - sWidth / 2, e.offsetY - sHeight / 2];
const scaleFactor = clampZoom(
viewStore.zoom,
@@ -65,28 +205,37 @@ export function useInputMachine(
translate(-dx, -dy),
viewStore.viewMatrix,
);
- });
+ },
+ };
- let dragState: [Matrix, number, number] | null = null;
- el.addEventListener('pointerdown', (e) => {
- if (e.button === 1) {
- el.style.cursor = 'grabbing';
- dragState = [viewStore.viewMatrix, e.offsetX, e.offsetY];
+ const pointerHandlers = {
+ down: (e: PointerEvent) => {
+ const isPanning =
+ e.button === 1 ||
+ (toolbarStore.selectedTool === 'pan' && e.button === 0);
+
+ if (isPanning) {
+ dragStateRef.value = [viewStore.viewMatrix, e.offsetX, e.offsetY];
}
- });
-
- el.addEventListener('pointermove', (e) => {
- if (!dragState) return;
- const [matrix, ix, iy] = dragState;
- const dx = ix - e.offsetX;
- const dy = iy - e.offsetY;
- viewStore.viewMatrix = compose(translate(-dx, -dy), matrix);
- });
-
- el.addEventListener('pointerup', () => {
- if (!dragState) return;
- el.style.cursor = '';
- dragState = null;
- });
+ },
+ move: (e: PointerEvent) => {
+ cursorPositionRef.value = [e.offsetX, e.offsetY];
+ },
+ up: () => {
+ if (!dragStateRef.value) return;
+ dragStateRef.value = null;
+ },
+ };
+
+ onMounted(() => {
+ const el = unref(stageRef);
+ if (!el) return;
+
+ el.addEventListener('click', mouseHandlers.click);
+ el.addEventListener('contextmenu', mouseHandlers.contextMenu);
+ el.addEventListener('wheel', mouseHandlers.wheel);
+ el.addEventListener('pointerdown', pointerHandlers.down);
+ el.addEventListener('pointermove', pointerHandlers.move);
+ el.addEventListener('pointerup', pointerHandlers.up);
});
}
diff --git a/apps/scishop/src/composables/useRafRef.ts b/apps/scishop/src/composables/useRafRef.ts
new file mode 100644
index 0000000..ec645dd
--- /dev/null
+++ b/apps/scishop/src/composables/useRafRef.ts
@@ -0,0 +1,21 @@
+import { customRef } from 'vue';
+
+export function useRafRef(initialValue: T) {
+ let value = initialValue;
+ let rafHandle: number | null = null;
+
+ return customRef((track, trigger) => ({
+ get() {
+ track();
+ return value;
+ },
+ set(newValue) {
+ if (rafHandle) cancelAnimationFrame(rafHandle);
+ rafHandle = requestAnimationFrame(() => {
+ value = newValue;
+ trigger();
+ rafHandle = null;
+ });
+ },
+ }));
+}
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
index 01bf5a8..0570a1b 100644
--- a/apps/scishop/src/data/picStore.ts
+++ b/apps/scishop/src/data/picStore.ts
@@ -1,6 +1,7 @@
import { ref, shallowRef, unref } from 'vue';
import { DrawCommand } from '@4bitlabs/sci0';
+import { insert } from '../helpers/array-helpers.ts';
const data: DrawCommand[] = [];
@@ -22,3 +23,24 @@ export default {
topIdxRef.value = n;
},
};
+
+const currentCommandRef = shallowRef(null);
+
+export const currentCommandStore = {
+ get current() {
+ return unref(currentCommandRef);
+ },
+ set current(cmd: DrawCommand | null) {
+ currentCommandRef.value = cmd;
+ },
+ commit() {
+ const cmd = unref(currentCommandRef);
+ if (cmd === null) return;
+ currentCommandRef.value = null;
+ layersRef.value = insert(layersRef.value, unref(topIdxRef) + 1, cmd);
+ topIdxRef.value += 1;
+ },
+ abort() {
+ currentCommandRef.value = null;
+ },
+};
diff --git a/apps/scishop/src/helpers/array-helpers.ts b/apps/scishop/src/helpers/array-helpers.ts
new file mode 100644
index 0000000..f2ee02e
--- /dev/null
+++ b/apps/scishop/src/helpers/array-helpers.ts
@@ -0,0 +1,10 @@
+export const insert = (
+ source: T[],
+ index: number,
+ item: T,
+ replaceAtIndex = false,
+): T[] => [
+ ...source.slice(0, index),
+ item,
+ ...source.slice(replaceAtIndex ? index + 1 : index),
+];
diff --git a/apps/scishop/src/helpers/polygons.ts b/apps/scishop/src/helpers/polygons.ts
new file mode 100644
index 0000000..052479f
--- /dev/null
+++ b/apps/scishop/src/helpers/polygons.ts
@@ -0,0 +1,36 @@
+export const isInsideBounds = (
+ [width, height]: [number, number],
+ [x, y]: [number, number],
+): boolean => x >= 0 && y >= 0 && x < width && y < height;
+
+export const pixel = (
+ [x, y]: [number, number],
+ offset = 0.0,
+): [number, number][] => [
+ [Math.floor(x) + offset, Math.floor(y) + offset],
+ [Math.ceil(x) - offset, Math.floor(y) + offset],
+ [Math.ceil(x) - offset, Math.ceil(y) - offset],
+ [Math.floor(x) + offset, Math.ceil(y) - offset],
+];
+
+export const areaOfPolygon = (points: [number, number][]) => {
+ const { length } = points;
+ let area = 0;
+
+ for (let i = 0; i < length; i++) {
+ const [x1, y1] = points[i];
+ const [x2, y2] = points[(i + 1) % length];
+ area += x1 * y2 - x2 * y1;
+ }
+
+ return Math.abs(area) / 2;
+};
+
+export const pathPoly = (
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ points: [number, number][],
+) => {
+ ctx.beginPath();
+ points.forEach(([x, y], i) => ctx[i === 0 ? 'moveTo' : 'lineTo'](x, y));
+ ctx.closePath();
+};
diff --git a/apps/scishop/src/helpers/smoothstep.ts b/apps/scishop/src/helpers/smoothstep.ts
new file mode 100644
index 0000000..b3c9780
--- /dev/null
+++ b/apps/scishop/src/helpers/smoothstep.ts
@@ -0,0 +1,20 @@
+const clamp = (x: number, lower: number = 0, upper: number = 1) =>
+ Math.max(Math.min(x, upper), lower);
+
+export const s0 = (left: number, right: number, val: number) =>
+ clamp((val - left) / (right - left));
+
+export const s1 = (left: number, right: number, val: number) => {
+ const x = clamp((val - left) / (right - left));
+ return x ** 2 * (-2.0 * x + 3.0);
+};
+
+export const s2 = (left: number, right: number, val: number) => {
+ const x = clamp((val - left) / (right - left));
+ return x ** 3 * (6 * x ** 2 - 15 * x + 10);
+};
+
+export const s3 = (left: number, right: number, val: number) => {
+ const x = clamp((val - left) / (right - left));
+ return -20 * x ** 7 + 70 * x ** 6 + -84 * x ** 5 + 35 * x ** 4;
+};
diff --git a/apps/scishop/src/helpers/vec2-helpers.ts b/apps/scishop/src/helpers/vec2-helpers.ts
new file mode 100644
index 0000000..cd8ac12
--- /dev/null
+++ b/apps/scishop/src/helpers/vec2-helpers.ts
@@ -0,0 +1,7 @@
+export type Vec2 = [number, number];
+export type StaticVec2 = readonly [number, number];
+
+export const intVec2 = (
+ [x, y]: StaticVec2,
+ fn: (v: number) => number = Math.floor,
+): StaticVec2 => [fn(x), fn(y)];
From a396363792881e7a96daaef29d33c40b90b44f44 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Sun, 26 May 2024 07:51:56 -0600
Subject: [PATCH 08/28] WIP some more tweaks before a refactor into
EditorCommands
---
apps/scishop/src/components/Stage.vue | 14 +++++++++----
.../src/composables/useCanvasRenderer.ts | 20 +++++++++++++------
apps/scishop/src/data/picStore.ts | 4 ++++
apps/scishop/src/helpers/array-helpers.ts | 11 ++++++++++
4 files changed, 39 insertions(+), 10 deletions(-)
diff --git a/apps/scishop/src/components/Stage.vue b/apps/scishop/src/components/Stage.vue
index 8136385..1e0b8ec 100644
--- a/apps/scishop/src/components/Stage.vue
+++ b/apps/scishop/src/components/Stage.vue
@@ -7,7 +7,10 @@ import {
applyToPoints,
} from 'transformation-matrix';
import { useResizeWatcher } from '../composables/useResizeWatcher';
-import { useCanvasRenderer } from '../composables/useCanvasRenderer';
+import {
+ useCanvasRenderer,
+ useRenderedPixels,
+} from '../composables/useCanvasRenderer';
import store, { currentCommandStore } from '../data/picStore';
import stageStore from '../data/stageStore';
import viewStore from '../data/viewStore';
@@ -21,9 +24,11 @@ const stageRes = useResizeWatcher(stageRef, 100);
const viewStack = computed(() => [
...store.layers.slice(0, store.topIdx + 1),
- ...(currentCommandStore.current ? [currentCommandStore.current] : []),
+ ...currentCommandStore.commands,
]);
-const pixels = useCanvasRenderer(viewStack, stageStore.canvasRes);
+
+const renderResult = useRenderedPixels(viewStack, stageStore.canvasRes);
+const pixels = useCanvasRenderer(renderResult, stageStore.canvasRes);
const matrixRef = computed(() => {
const [sWidth, sHeight] = unref(stageRes);
@@ -36,7 +41,7 @@ const matrixRef = computed(() => {
);
});
-const smootherizeRef = computed(() => viewStore.zoom < 15);
+const smootherizeRef = computed(() => viewStore.zoom < 12);
watch(
[stageRef, stageRes, pixels, stageStore.canvasRes, matrixRef, smootherizeRef],
@@ -67,6 +72,7 @@ watch(
ctx.shadowColor = `rgba(0 0 0 / 25%)`;
ctx.shadowBlur = 25;
ctx.shadowOffsetY = 10;
+ ctx.shadowOffsetX = 10;
ctx.fillStyle = `black`;
ctx.fill();
ctx.restore();
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index baa1049..f0e8312 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,4 +1,5 @@
import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
+import { RenderResult } from '@4bitlabs/sci0/dist/screen/render-result.ts';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
import { createDitherFilter, renderPixelData } from '@4bitlabs/image';
@@ -13,21 +14,28 @@ import { get2dContext } from '../helpers/getContext';
import viewStore from '../data/viewStore.ts';
const oversampleRef = computed<[number, number]>(() => {
- const samples = Math.min(Math.max(1, Math.ceil(viewStore.zoom)), 5);
+ if (viewStore.zoom > 12) return [1, 1];
+ const samples = Math.min(Math.max(1, Math.round(viewStore.zoom)), 5);
+
return [samples, samples];
});
-export function useCanvasRenderer(
+export function useRenderedPixels(
picDataRef: Ref,
resRef: Ref<[number, number]>,
-): Ref {
- const canvasRef = shallowRef(new OffscreenCanvas(1, 1));
-
- const renderedRef = computed(() => {
+) {
+ return computed(() => {
const picData = unref(picDataRef);
const [width, height] = unref(resRef);
return renderPic(picData, { width, height });
});
+}
+
+export function useCanvasRenderer(
+ renderedRef: Ref,
+ resRef: Ref<[number, number]>,
+): Ref {
+ const canvasRef = shallowRef(new OffscreenCanvas(1, 1));
watch(
[renderedRef, resRef, oversampleRef],
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
index 0570a1b..5c12c66 100644
--- a/apps/scishop/src/data/picStore.ts
+++ b/apps/scishop/src/data/picStore.ts
@@ -33,6 +33,10 @@ export const currentCommandStore = {
set current(cmd: DrawCommand | null) {
currentCommandRef.value = cmd;
},
+ get commands() {
+ const cmd = unref(currentCommandRef);
+ return cmd ? [cmd] : [];
+ },
commit() {
const cmd = unref(currentCommandRef);
if (cmd === null) return;
diff --git a/apps/scishop/src/helpers/array-helpers.ts b/apps/scishop/src/helpers/array-helpers.ts
index f2ee02e..163ffce 100644
--- a/apps/scishop/src/helpers/array-helpers.ts
+++ b/apps/scishop/src/helpers/array-helpers.ts
@@ -8,3 +8,14 @@ export const insert = (
item,
...source.slice(replaceAtIndex ? index + 1 : index),
];
+
+export const insertN = (
+ source: T[],
+ index: number,
+ items: T[],
+ replace = 0,
+): T[] => [
+ ...source.slice(0, index),
+ ...items,
+ ...source.slice(index + Math.max(0, replace)),
+];
From a73bf268197fed635a1b6ae121312a876b1d1981 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Sun, 26 May 2024 09:48:20 -0600
Subject: [PATCH 09/28] WIP refactored low-level DrawCommand stacks into
EditorCommand stacks
---
.../scishop/src/components/LayerNavigator.vue | 24 ++---
.../src/components/PolyLineCommandItem.vue | 97 -------------------
apps/scishop/src/components/Stage.vue | 2 +-
.../command-items/BrushCommandItem.vue | 23 +++++
.../command-items/FillCommandItem.vue | 18 ++++
.../GenericCommandItem.vue} | 0
.../command-items/PolyLineCommandItem.vue | 18 ++++
.../src/components/command-items/Swatches.vue | 95 ++++++++++++++++++
apps/scishop/src/data/picStore.ts | 21 +++-
apps/scishop/src/helpers/getPals.ts | 12 +--
apps/scishop/src/models/EditorCommand.ts | 34 +++++++
11 files changed, 225 insertions(+), 119 deletions(-)
delete mode 100644 apps/scishop/src/components/PolyLineCommandItem.vue
create mode 100644 apps/scishop/src/components/command-items/BrushCommandItem.vue
create mode 100644 apps/scishop/src/components/command-items/FillCommandItem.vue
rename apps/scishop/src/components/{DrawCommandItem.vue => command-items/GenericCommandItem.vue} (100%)
create mode 100644 apps/scishop/src/components/command-items/PolyLineCommandItem.vue
create mode 100644 apps/scishop/src/components/command-items/Swatches.vue
create mode 100644 apps/scishop/src/models/EditorCommand.ts
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index 1333185..f25814f 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -2,19 +2,21 @@
import { computed } from 'vue';
import { mapToPals } from '../helpers/getPals';
import store from '../data/picStore';
-import DrawCommandItem from './DrawCommandItem.vue';
-import PolyLineCommandItem from './PolyLineCommandItem.vue';
+import GenericCommandItem from './command-items/GenericCommandItem.vue';
+import PolyLineCommandItem from './command-items/PolyLineCommandItem.vue';
+import FillCommandItem from './command-items/FillCommandItem.vue';
+import BrushCommandItem from './command-items/BrushCommandItem.vue';
const stack = computed(() => Array.from(store.layers.entries()).reverse());
const stackPalettes = computed(() => mapToPals(store.layers));
const itemType = {
- SET_PALETTE: DrawCommandItem,
- UPDATE_PALETTE: DrawCommandItem,
- BRUSH: PolyLineCommandItem,
- FILL: PolyLineCommandItem,
+ SET_PALETTE: GenericCommandItem,
+ UPDATE_PALETTE: GenericCommandItem,
+ BRUSH: BrushCommandItem,
+ FILL: FillCommandItem,
PLINE: PolyLineCommandItem,
- CEL: DrawCommandItem,
+ CEL: GenericCommandItem,
};
@@ -28,10 +30,10 @@ const itemType = {
C
-import {
- PolylineCommand,
- isVisualMode,
- isPriorityMode,
- isControlMode,
- BrushCommand,
- FillCommand,
-} from '@4bitlabs/sci0';
-import { computed } from 'vue';
-import { Palettes } from '@4bitlabs/color';
-import { fromUint32, toHex } from '@4bitlabs/color-space/srgb';
-
-const { command, pals } = defineProps<{
- command: PolylineCommand | BrushCommand | FillCommand;
- pals: [number[], number[], number[], number[]];
-}>();
-
-const [name, drawMode, code] = command;
-
-const getVisualPair = computed(() => {
- const pal = (code[0] / 40) >>> 0;
- const idx = code[0] % 40;
- const pair = pals[pal][idx];
-
- const [aClr, bClr] = [
- Palettes.TRUE_CGA_PALETTE[pair >>> 4],
- Palettes.TRUE_CGA_PALETTE[pair & 0b1111],
- ];
-
- return {
- '--dither-left': toHex(fromUint32(aClr)),
- '--dither-right': toHex(fromUint32(bClr)),
- };
-});
-
-const getPriorityColor = computed(() => ({
- background: toHex(fromUint32(Palettes.CGA_PALETTE[code[1]])),
- color: code[1] > 8 ? '#000' : '#fff',
-}));
-
-const getControlColor = computed(() => ({
- background: toHex(fromUint32(Palettes.CGA_PALETTE[code[1]])),
- color: code[1] > 8 ? '#000' : '#fff',
-}));
-
-
-
-
- {{ name }}
-
-
- {{
- code[1].toString(16)
- }}
-
-
- {{
- code[2].toString(16)
- }}
-
-
-
-
-
diff --git a/apps/scishop/src/components/Stage.vue b/apps/scishop/src/components/Stage.vue
index 1e0b8ec..31fd3f5 100644
--- a/apps/scishop/src/components/Stage.vue
+++ b/apps/scishop/src/components/Stage.vue
@@ -23,7 +23,7 @@ const uiRef = shallowRef(null);
const stageRes = useResizeWatcher(stageRef, 100);
const viewStack = computed(() => [
- ...store.layers.slice(0, store.topIdx + 1),
+ ...store.layers.slice(0, store.topIdx + 1).flatMap((it) => it.commands),
...currentCommandStore.commands,
]);
diff --git a/apps/scishop/src/components/command-items/BrushCommandItem.vue b/apps/scishop/src/components/command-items/BrushCommandItem.vue
new file mode 100644
index 0000000..9b2e24b
--- /dev/null
+++ b/apps/scishop/src/components/command-items/BrushCommandItem.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+ {{ isSpray ? 'Spray' : 'Solid' }}
+ {{ isRect ? 'Square' : 'Circle' }}
+ Brush Size {{ size }}
+
+
+
+
diff --git a/apps/scishop/src/components/command-items/FillCommandItem.vue b/apps/scishop/src/components/command-items/FillCommandItem.vue
new file mode 100644
index 0000000..a471313
--- /dev/null
+++ b/apps/scishop/src/components/command-items/FillCommandItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+ Fill
+
+
+
diff --git a/apps/scishop/src/components/DrawCommandItem.vue b/apps/scishop/src/components/command-items/GenericCommandItem.vue
similarity index 100%
rename from apps/scishop/src/components/DrawCommandItem.vue
rename to apps/scishop/src/components/command-items/GenericCommandItem.vue
diff --git a/apps/scishop/src/components/command-items/PolyLineCommandItem.vue b/apps/scishop/src/components/command-items/PolyLineCommandItem.vue
new file mode 100644
index 0000000..b920968
--- /dev/null
+++ b/apps/scishop/src/components/command-items/PolyLineCommandItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+ Line ({{ coords.length }})
+
+
+
diff --git a/apps/scishop/src/components/command-items/Swatches.vue b/apps/scishop/src/components/command-items/Swatches.vue
new file mode 100644
index 0000000..c25e2ce
--- /dev/null
+++ b/apps/scishop/src/components/command-items/Swatches.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+ {{
+ drawCode[1].toString(16)
+ }}
+
+
+ {{
+ drawCode[2].toString(16)
+ }}
+
+
+
+
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
index 5c12c66..4484e66 100644
--- a/apps/scishop/src/data/picStore.ts
+++ b/apps/scishop/src/data/picStore.ts
@@ -1,11 +1,17 @@
-import { ref, shallowRef, unref } from 'vue';
+import { computed, ref, shallowRef, unref } from 'vue';
import { DrawCommand } from '@4bitlabs/sci0';
import { insert } from '../helpers/array-helpers.ts';
+import { EditorCommand } from '../models/EditorCommand.ts';
-const data: DrawCommand[] = [];
+const nextId = () => Math.random().toString(36).substring(2);
-const layersRef = shallowRef(data);
+const data: EditorCommand[] = [];
+
+const layersRef = shallowRef(data);
+const commandsRef = computed(() =>
+ unref(layersRef).flatMap((it) => it.commands),
+);
const selectedCommandIdx = ref(null);
const topIdxRef = ref(data.length - 1);
@@ -13,6 +19,9 @@ export default {
get layers() {
return unref(layersRef);
},
+ get commands() {
+ return unref(commandsRef);
+ },
get cmdIdx() {
return unref(selectedCommandIdx);
},
@@ -41,7 +50,11 @@ export const currentCommandStore = {
const cmd = unref(currentCommandRef);
if (cmd === null) return;
currentCommandRef.value = null;
- layersRef.value = insert(layersRef.value, unref(topIdxRef) + 1, cmd);
+ layersRef.value = insert(layersRef.value, unref(topIdxRef) + 1, {
+ id: nextId(),
+ type: cmd[0],
+ commands: [cmd],
+ });
topIdxRef.value += 1;
},
abort() {
diff --git a/apps/scishop/src/helpers/getPals.ts b/apps/scishop/src/helpers/getPals.ts
index 4fa6bdd..cc7a7fd 100644
--- a/apps/scishop/src/helpers/getPals.ts
+++ b/apps/scishop/src/helpers/getPals.ts
@@ -1,4 +1,4 @@
-import { DrawCommand } from '@4bitlabs/sci0';
+import { EditorCommand } from '../models/EditorCommand.ts';
// prettier-ignore
export const DEFAULT_PALETTE: number[] = [
@@ -10,9 +10,9 @@ export const DEFAULT_PALETTE: number[] = [
type PaletteSet = [number[], number[], number[], number[]];
-export const mapToPals = (commands: DrawCommand[]) => {
+export const mapToPals = (commands: EditorCommand[]) => {
return commands.reduce(
- (stack: PaletteSet[], cmd: DrawCommand) => {
+ (stack: PaletteSet[], editorCmd: EditorCommand) => {
const prevSet = stack[stack.length - 1] ?? [
DEFAULT_PALETTE,
DEFAULT_PALETTE,
@@ -20,16 +20,16 @@ export const mapToPals = (commands: DrawCommand[]) => {
DEFAULT_PALETTE,
];
- const [type] = cmd;
+ const { type } = editorCmd;
switch (type) {
case 'SET_PALETTE': {
- const [, palIdx, colors] = cmd;
+ const [, palIdx, colors] = editorCmd.commands[0];
const next: PaletteSet = [...prevSet];
next[palIdx] = [...colors];
return [...stack, next];
}
case 'UPDATE_PALETTE': {
- const [, entries] = cmd;
+ const [, entries] = editorCmd.commands[0];
const next = entries.reduce((pals, [palIdx, idx, color]) => {
const nextSet: PaletteSet = [...prevSet];
const nextPal = [...nextSet[palIdx]];
diff --git a/apps/scishop/src/models/EditorCommand.ts b/apps/scishop/src/models/EditorCommand.ts
new file mode 100644
index 0000000..f057cc4
--- /dev/null
+++ b/apps/scishop/src/models/EditorCommand.ts
@@ -0,0 +1,34 @@
+import {
+ BrushCommand,
+ DrawCommand,
+ EmbeddedCelCommand,
+ FillCommand,
+ PolylineCommand,
+ SetPaletteCommand,
+ UpdatePaletteCommand,
+} from '@4bitlabs/sci0';
+
+export type BasicEditorCommand = {
+ readonly id: symbol | number | string;
+ readonly type: T[0];
+ readonly name?: string;
+ readonly commands: readonly [T];
+};
+
+export type CompositeEditorCommand = {
+ readonly id: symbol | number | string;
+ readonly type: T;
+ readonly name?: string;
+ readonly commands: readonly (PolylineCommand | FillCommand | BrushCommand)[];
+};
+
+export type GroupEditorCommand = CompositeEditorCommand<'group'>;
+
+export type EditorCommand =
+ | GroupEditorCommand
+ | BasicEditorCommand
+ | BasicEditorCommand
+ | BasicEditorCommand
+ | BasicEditorCommand
+ | BasicEditorCommand
+ | BasicEditorCommand;
From d6d3cf2e973972e1a62a4bc23b3ef1c4630b6bc9 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Tue, 28 May 2024 07:31:45 -0600
Subject: [PATCH 10/28] WIP prototyping on scishop
---
apps/scishop/src/canvas/drawHelpers.ts | 58 ------
apps/scishop/src/components/Header.vue | 52 ++++-
.../scishop/src/components/LayerNavigator.vue | 73 ++++---
apps/scishop/src/components/PalettePicker.vue | 148 -------------
.../components/PalettePicker/ModeSelector.vue | 75 +++++++
.../PalettePicker/PalettePicker.vue | 157 ++++++++++++++
.../src/components/PalettePicker/index.ts | 2 +
apps/scishop/src/components/Sidebar.vue | 10 +-
apps/scishop/src/components/Stage.vue | 2 +-
apps/scishop/src/components/Toolbar.vue | 1 -
.../command-items/SetPaletteCommand.vue | 10 +
.../src/components/command-items/Swatches.vue | 24 +--
.../src/composables/useCanvasRenderer.ts | 22 +-
.../src/composables/useInputMachine.ts | 196 ++++++++++++------
apps/scishop/src/data/paletteStore.ts | 94 +++++++++
apps/scishop/src/data/picStore.ts | 43 ++--
.../{getPals.ts => palette-helpers.ts} | 16 +-
apps/scishop/src/helpers/vec2-helpers.ts | 5 +-
apps/scishop/src/models/EditorCommand.ts | 4 +-
apps/scishop/src/render/cursor-dot.ts | 14 ++
apps/scishop/src/render/fill-skeleton.ts | 28 +++
apps/scishop/src/render/pixel-border.ts | 21 ++
apps/scishop/src/render/pline-skeleton.ts | 32 +++
apps/scishop/src/reset.css | 4 +-
apps/scishop/src/style.css | 15 +-
apps/scishop/tsconfig.json | 4 +-
26 files changed, 734 insertions(+), 376 deletions(-)
delete mode 100644 apps/scishop/src/canvas/drawHelpers.ts
delete mode 100644 apps/scishop/src/components/PalettePicker.vue
create mode 100644 apps/scishop/src/components/PalettePicker/ModeSelector.vue
create mode 100644 apps/scishop/src/components/PalettePicker/PalettePicker.vue
create mode 100644 apps/scishop/src/components/PalettePicker/index.ts
create mode 100644 apps/scishop/src/components/command-items/SetPaletteCommand.vue
create mode 100644 apps/scishop/src/data/paletteStore.ts
rename apps/scishop/src/helpers/{getPals.ts => palette-helpers.ts} (77%)
create mode 100644 apps/scishop/src/render/cursor-dot.ts
create mode 100644 apps/scishop/src/render/fill-skeleton.ts
create mode 100644 apps/scishop/src/render/pixel-border.ts
create mode 100644 apps/scishop/src/render/pline-skeleton.ts
diff --git a/apps/scishop/src/canvas/drawHelpers.ts b/apps/scishop/src/canvas/drawHelpers.ts
deleted file mode 100644
index de0e35a..0000000
--- a/apps/scishop/src/canvas/drawHelpers.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { applyToPoints, Matrix } from 'transformation-matrix';
-
-import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
-
-export const drawFILL = (
- ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
- matrix: Matrix,
- cmd: FillCommand,
-) => {
- const [, , , ...points] = cmd;
- const all = applyToPoints(
- matrix,
- points.map(([x, y]) => [x + 0.5, y + 0.5]),
- );
-
- all.forEach(([x, y]) => {
- ctx.save();
- ctx.translate(x, y);
- ctx.beginPath();
- ctx.moveTo(3, 0);
- ctx.lineTo(0, -4);
- ctx.lineTo(-3, 0);
- ctx.lineTo(0, 4);
- ctx.lineTo(3, 0);
- ctx.stroke();
- ctx.restore();
- });
-};
-
-export const drawPLINE = (
- ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
- matrix: Matrix,
- cmd: PolylineCommand,
-) => {
- const [, , , ...points] = cmd;
- const all = applyToPoints(
- matrix,
- points.map(([x, y]) => [x + 0.5, y + 0.5]),
- );
-
- {
- ctx.beginPath();
- const [first, ...rest] = all;
- ctx.moveTo(...first);
- rest.forEach(([x, y]) => ctx.lineTo(x, y));
-
- ctx.stroke();
- }
-
- all.forEach(([x, y]) => {
- ctx.save();
- ctx.translate(x, y);
- ctx.beginPath();
- ctx.roundRect(-3, -3, 6, 6, 0.5);
- ctx.stroke();
- ctx.restore();
- });
-};
diff --git a/apps/scishop/src/components/Header.vue b/apps/scishop/src/components/Header.vue
index 72ee1a9..0e4ba84 100644
--- a/apps/scishop/src/components/Header.vue
+++ b/apps/scishop/src/components/Header.vue
@@ -1,14 +1,60 @@
-
+
+
+
+ Pal
+
+
+
+
+
+
+
+
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index f25814f..95b3747 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -1,22 +1,50 @@
@@ -32,16 +60,16 @@ const itemType = {
@@ -58,16 +86,9 @@ const itemType = {
align-items: start;
align-content: start;
flex-grow: 1;
- margin-bottom: 0.25lh;
padding-bottom: 0.5lh;
border-bottom-left-radius: 0.5lh;
- background-image: repeating-linear-gradient(
- -45deg,
- rgba(0 0 0 / 5%) 0px,
- rgba(0 0 0 / 5%) 5px,
- rgba(0 0 0 / 0%) 5px,
- rgba(0 0 0 / 0%) 10px
- );
+ background-image: var(--transparent-img);
}
.head {
@@ -87,7 +108,7 @@ const itemType = {
padding-inline-start: 0.5ch;
padding-block: 0.5lh 0.5ch;
grid-column: 1 / -1;
- border-bottom: 1px solid var(--clr-ink-A10);
+ border-bottom: 1px solid var(--clr-surface-200);
}
.head :nth-child(n + 2) {
@@ -120,7 +141,7 @@ const itemType = {
.top {
padding-block: 0.5lh;
- border-top: 2px dashed var(--clr-primary-800);
+ border-top: 2px dashed var(--clr-primary-700);
}
.current {
diff --git a/apps/scishop/src/components/PalettePicker.vue b/apps/scishop/src/components/PalettePicker.vue
deleted file mode 100644
index ce2b439..0000000
--- a/apps/scishop/src/components/PalettePicker.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/scishop/src/components/PalettePicker/ModeSelector.vue b/apps/scishop/src/components/PalettePicker/ModeSelector.vue
new file mode 100644
index 0000000..619df9e
--- /dev/null
+++ b/apps/scishop/src/components/PalettePicker/ModeSelector.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
diff --git a/apps/scishop/src/components/PalettePicker/PalettePicker.vue b/apps/scishop/src/components/PalettePicker/PalettePicker.vue
new file mode 100644
index 0000000..51062e2
--- /dev/null
+++ b/apps/scishop/src/components/PalettePicker/PalettePicker.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/scishop/src/components/PalettePicker/index.ts b/apps/scishop/src/components/PalettePicker/index.ts
new file mode 100644
index 0000000..469ca1e
--- /dev/null
+++ b/apps/scishop/src/components/PalettePicker/index.ts
@@ -0,0 +1,2 @@
+import PalettePicker from './PalettePicker.vue';
+export default PalettePicker;
diff --git a/apps/scishop/src/components/Sidebar.vue b/apps/scishop/src/components/Sidebar.vue
index e0bc91f..d3d20a9 100644
--- a/apps/scishop/src/components/Sidebar.vue
+++ b/apps/scishop/src/components/Sidebar.vue
@@ -1,11 +1,11 @@
@@ -16,8 +16,8 @@ import LayerNavigator from './LayerNavigator.vue';
display: flex;
flex-direction: column;
color: var(--clr-ink);
- background-color: var(--clr-surface--default);
- gap: 3px;
- border-left: 3px solid var(--clr-surface--default);
+ background-color: var(--clr-surface-200);
+ gap: 0.25lh;
+ padding-block: 0.25lh;
}
diff --git a/apps/scishop/src/components/Stage.vue b/apps/scishop/src/components/Stage.vue
index 31fd3f5..2449208 100644
--- a/apps/scishop/src/components/Stage.vue
+++ b/apps/scishop/src/components/Stage.vue
@@ -23,7 +23,7 @@ const uiRef = shallowRef(null);
const stageRes = useResizeWatcher(stageRef, 100);
const viewStack = computed(() => [
- ...store.layers.slice(0, store.topIdx + 1).flatMap((it) => it.commands),
+ ...store.layers.slice(0, store.topIdx).flatMap((it) => [...it.commands]),
...currentCommandStore.commands,
]);
diff --git a/apps/scishop/src/components/Toolbar.vue b/apps/scishop/src/components/Toolbar.vue
index 8713a06..83462f7 100644
--- a/apps/scishop/src/components/Toolbar.vue
+++ b/apps/scishop/src/components/Toolbar.vue
@@ -141,7 +141,6 @@ import store from '../data/toolbarStore';
align-items: center;
justify-content: center;
aspect-ratio: 1 / 1;
- cursor: pointer;
user-select: none;
&:disabled {
diff --git a/apps/scishop/src/components/command-items/SetPaletteCommand.vue b/apps/scishop/src/components/command-items/SetPaletteCommand.vue
new file mode 100644
index 0000000..bc979fa
--- /dev/null
+++ b/apps/scishop/src/components/command-items/SetPaletteCommand.vue
@@ -0,0 +1,10 @@
+
+
+
+ {{ command[0] }}:{{ pal }}
+
diff --git a/apps/scishop/src/components/command-items/Swatches.vue b/apps/scishop/src/components/command-items/Swatches.vue
index c25e2ce..02f1e24 100644
--- a/apps/scishop/src/components/command-items/Swatches.vue
+++ b/apps/scishop/src/components/command-items/Swatches.vue
@@ -10,16 +10,16 @@ import {
import { Palettes } from '@4bitlabs/color';
import { fromUint32, toHex } from '@4bitlabs/color-space/srgb';
-const { drawCode, drawMode, pals } = defineProps<{
+const props = defineProps<{
drawMode: DrawMode;
drawCode: DrawCodes;
pals: [number[], number[], number[], number[]];
}>();
const getVisualPair = computed(() => {
- const pal = (drawCode[0] / 40) >>> 0;
- const idx = drawCode[0] % 40;
- const pair = pals[pal][idx];
+ const pal = (props.drawCode[0] / 40) >>> 0;
+ const idx = props.drawCode[0] % 40;
+ const pair = props.pals[pal][idx];
const [aClr, bClr] = [
Palettes.TRUE_CGA_PALETTE[pair >>> 4],
@@ -33,13 +33,13 @@ const getVisualPair = computed(() => {
});
const getPriorityColor = computed(() => ({
- background: toHex(fromUint32(Palettes.CGA_PALETTE[drawCode[1]])),
- color: drawCode[1] > 8 ? '#000' : '#fff',
+ background: toHex(fromUint32(Palettes.CGA_PALETTE[props.drawCode[1]])),
+ color: props.drawCode[1] > 8 ? '#000' : '#fff',
}));
const getControlColor = computed(() => ({
- background: toHex(fromUint32(Palettes.CGA_PALETTE[drawCode[1]])),
- color: drawCode[1] > 8 ? '#000' : '#fff',
+ background: toHex(fromUint32(Palettes.CGA_PALETTE[props.drawCode[1]])),
+ color: props.drawCode[1] > 8 ? '#000' : '#fff',
}));
@@ -72,13 +72,7 @@ const getControlColor = computed(() => ({
font-weight: bold;
border: 1px dotted var(--clr-ink-A10);
background-clip: padding-box;
- background-image: repeating-linear-gradient(
- -45deg,
- var(--clr-ink-A10) 0em,
- var(--clr-ink-A10) 0.5em,
- transparent 0.5em,
- transparent 1em
- );
+ background-image: var(--transparent-img);
}
.pair {
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index f0e8312..4de4656 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,17 +1,13 @@
-import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
+import { Ref, ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
import { RenderResult } from '@4bitlabs/sci0/dist/screen/render-result.ts';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
import { createDitherFilter, renderPixelData } from '@4bitlabs/image';
-import {
- generateSciDitherPairs,
- IBM5153Contrast,
- Mixers,
- Palettes,
-} from '@4bitlabs/color';
+import { generateSciDitherPairs, Mixers } from '@4bitlabs/color';
import { nearestNeighbor } from '@4bitlabs/resize-filters';
import { get2dContext } from '../helpers/getContext';
import viewStore from '../data/viewStore.ts';
+import { screenPalette as screenPaletteRef } from '../data/paletteStore.ts';
const oversampleRef = computed<[number, number]>(() => {
if (viewStore.zoom > 12) return [1, 1];
@@ -31,15 +27,17 @@ export function useRenderedPixels(
});
}
+const soft = ref(true);
+(window as unknown as any)['soft'] = soft;
+
export function useCanvasRenderer(
renderedRef: Ref,
resRef: Ref<[number, number]>,
): Ref {
const canvasRef = shallowRef(new OffscreenCanvas(1, 1));
-
watch(
- [renderedRef, resRef, oversampleRef],
- ([pic, [width, height], oversample]) => {
+ [renderedRef, resRef, oversampleRef, screenPaletteRef, soft],
+ ([pic, [width, height], oversample, palette, softOn]) => {
const canvas = unref(canvasRef);
canvas.width = width * oversample[0];
canvas.height = height * oversample[1];
@@ -47,8 +45,8 @@ export function useCanvasRenderer(
const imgData = renderPixelData(pic.visible, {
dither: createDitherFilter(
generateSciDitherPairs(
- IBM5153Contrast(Palettes.TRUE_CGA_PALETTE, 0.4),
- Mixers.softMixer(),
+ palette,
+ softOn ? Mixers.softMixer() : ([a, b]) => [a, b],
),
[1, 1],
),
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index a4bedb7..eb15368 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -7,6 +7,7 @@ import {
unref,
watch,
watchEffect,
+ onUnmounted,
} from 'vue';
import {
applyToPoint,
@@ -22,16 +23,18 @@ import {
import toolbarStore from '../data/toolbarStore';
import viewStore from '../data/viewStore';
import { get2dContext } from '../helpers/getContext.ts';
-import {
- isInsideBounds,
- pixel,
- areaOfPolygon,
- pathPoly,
-} from '../helpers/polygons.ts';
-import * as SmoothStep from '../helpers/smoothstep';
+import { isInsideBounds, pixel } from '../helpers/polygons.ts';
import { useRafRef } from './useRafRef.ts';
-import { currentCommandStore as cmdStore } from '../data/picStore.ts';
+import picStore, {
+ currentCommandStore,
+ currentCommandStore as cmdStore,
+} from '../data/picStore.ts';
import { intVec2 } from '../helpers/vec2-helpers.ts';
+import { drawState } from '../data/paletteStore.ts';
+import { pixelBorder } from '../render/pixel-border.ts';
+import { fillSkeleton } from '../render/fill-skeleton.ts';
+import { plineSkeleton } from '../render/pline-skeleton.ts';
+import { cursorDot } from '../render/cursor-dot.ts';
const clampZoom = (current: number, next: number, min: number, max: number) => {
if (current * next < min) return min / current;
@@ -43,31 +46,55 @@ type CSSCursor = string;
export function useInputMachine(
matrixRef: Ref,
- stageRef: Ref,
+ canvasRef: Ref,
stageResRef: Ref<[number, number]>,
canvasResRef: Ref<[number, number]>,
) {
const iMatrixRef = computed(() => inverse(unref(matrixRef)));
const dragStateRef = shallowRef<[Matrix, number, number] | null>(null);
const currentCursorRef = ref('auto');
- const cursorPositionRef = useRafRef<[number, number]>([0, 0]);
- const projected = computed((prev) => {
+ const selectedLayerRef = computed(() => {
+ const current = unref(currentCommandStore.current);
+ if (current) return current;
+ const selIdx = unref(picStore.selection);
+ return selIdx !== null ? unref(picStore.layers)[selIdx] : null;
+ });
+
+ const cursorPositionRef = useRafRef<[number, number]>([0, 0]);
+ const canvasPositionRef = computed<[number, number]>(() => {
const sPos = unref(cursorPositionRef);
- const next = intVec2(applyToPoint(unref(iMatrixRef), sPos));
+ const iMatrix = unref(iMatrixRef);
+ return applyToPoint(iMatrix, sPos);
+ });
+ const canvasPixelRef = computed<[number, number]>((prev) => {
+ const next = intVec2(unref(canvasPositionRef));
const isSame = prev && prev[0] === next[0] && prev[1] === next[1];
return isSame ? prev : next;
});
+ const isOverCanvasRef = computed(() => {
+ const canvasPoint = unref(canvasPositionRef);
+ const [cWidth, cHeight] = unref(canvasResRef);
+ return isInsideBounds([cWidth, cHeight], canvasPoint);
+ });
+
+ const isCursorHiddenRef = computed(() => {
+ const isTool = ['line', 'fill'].includes(toolbarStore.selectedTool);
+ const isOverCanvas = unref(isOverCanvasRef);
+ return isTool && isOverCanvas;
+ });
+
watchEffect(() => {
- const el = unref(stageRef);
+ const el = unref(canvasRef);
if (!el) return;
- el.style.cursor = currentCursorRef.value;
+ const hidden = unref(isCursorHiddenRef);
+ const currentCursor = unref(currentCursorRef);
+ el.style.cursor = hidden ? 'none' : currentCursor;
});
watch([cursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
if (!dragState) return;
-
const [matrix, ix, iy] = dragState;
const dx = ix - cX;
const dy = iy - cY;
@@ -75,8 +102,14 @@ export function useInputMachine(
});
watch(
- [stageRef, matrixRef, iMatrixRef, cursorPositionRef],
- ([el, matrix, iMatrix, screenPoint]) => {
+ [
+ canvasRef,
+ matrixRef,
+ cursorPositionRef,
+ canvasPositionRef,
+ selectedLayerRef,
+ ],
+ ([el, matrix, screenPoint, canvasPoint, selectedLayer]) => {
if (!el) return;
const [sWidth, sHeight] = unref(stageResRef);
@@ -85,49 +118,57 @@ export function useInputMachine(
const ctx = get2dContext(el);
ctx.clearRect(0, 0, sWidth, sHeight);
- if (toolbarStore.selectedTool === 'line') {
- const canvasPoint = applyToPoint(iMatrix, screenPoint);
+ // Draw precision cursor
+ if (
+ toolbarStore.selectedTool === 'line' ||
+ toolbarStore.selectedTool === 'fill'
+ ) {
+ // const canvasPoint = applyToPoint(iMatrix, screenPoint);
const [cWidth, cHeight] = unref(canvasResRef);
-
- ctx.lineWidth = 1;
- ctx.strokeStyle = 'white';
const overCanvas = isInsideBounds([cWidth, cHeight], canvasPoint);
- currentCursorRef.value = overCanvas ? 'none' : 'auto';
- if (overCanvas) {
- const pixelBounds = applyToPoints(matrix, pixel(canvasPoint, 0.0125));
- const area = areaOfPolygon(pixelBounds);
- const alpha = SmoothStep.s1(1, 6 * 6, area);
+ if (overCanvas) {
ctx.save();
- pathPoly(ctx, pixelBounds);
- ctx.setLineDash([1, 1]);
- ctx.strokeStyle = `oklab(${(alpha * 100).toFixed(0)}% 0 0)`;
- ctx.fillStyle = `oklab(${(alpha * 100 * 0.45).toFixed(0)}% 0 0)`;
- ctx.fill();
- ctx.stroke();
+ pixelBorder(ctx, applyToPoints(matrix, pixel(canvasPoint, 0.0125)));
ctx.restore();
- ctx.fillStyle = 'white';
- ctx.beginPath();
- ctx.arc(...screenPoint, 2, 0, Math.PI * 2);
- ctx.fill();
+ ctx.save();
+ cursorDot(ctx, screenPoint);
+ ctx.restore();
}
}
+
+ if (selectedLayer) {
+ ctx.save();
+ ctx.lineWidth = 1.5;
+ selectedLayer.commands.forEach((cmd) => {
+ const [type] = cmd;
+ ctx.strokeStyle = 'white';
+ if (type === 'PLINE') {
+ plineSkeleton(ctx, matrix, cmd);
+ } else if (type === 'FILL') {
+ fillSkeleton(ctx, matrix, cmd);
+ }
+ });
+ }
},
);
- watch([projected], ([pos]) => {
+ watch([canvasPixelRef], ([pos]) => {
const current = cmdStore.current;
if (current === null) return;
- if (current[0] === 'PLINE') {
- const [id, mode, code, ...coords] = current;
- cmdStore.current = [id, mode, code, ...coords.slice(0, -1), pos];
+ if (current.type === 'PLINE') {
+ const [type, mode, code, ...coords] = current.commands[0];
+ cmdStore.begin({
+ ...current,
+ commands: [[type, mode, code, ...coords.slice(0, -1), pos]],
+ });
}
});
watchEffect(() => {
- const el = unref(stageRef);
+ const el = unref(canvasRef);
if (!el) return;
const stateState = unref(dragStateRef);
@@ -146,25 +187,39 @@ export function useInputMachine(
const mouseHandlers = {
click: (e: MouseEvent) => {
- console.log(e.button);
- if (toolbarStore.selectedTool === 'line') {
- const [x, y] = applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]);
- const pos: readonly [number, number] = [Math.floor(x), Math.floor(y)];
+ if (toolbarStore.selectedTool === 'fill') {
+ const pos = intVec2(
+ applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
+ );
+ const [drawMode, ...drawCodes] = unref(drawState);
+ picStore.selection = cmdStore.commit({
+ id: Math.random().toString(36).substring(2),
+ type: 'FILL',
+ commands: [['FILL', drawMode, drawCodes, pos]],
+ });
+ } else if (toolbarStore.selectedTool === 'line') {
+ const pos = intVec2(
+ applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
+ );
const current = cmdStore.current;
+ const [drawMode, ...drawCodes] = unref(drawState);
if (current === null) {
- cmdStore.current = [
- 'PLINE',
- 1,
- [Math.floor(Math.random() * 40), 0, 0],
- pos,
- pos,
- ];
- } else if (current[0] === 'PLINE') {
- const [id, mode, code, ...coords] = current;
- cmdStore.current = [id, mode, code, ...coords.slice(0, -1), pos, pos];
+ cmdStore.begin({
+ id: Math.random().toString(36).substring(2),
+ type: 'PLINE',
+ commands: [['PLINE', drawMode, drawCodes, pos, pos]],
+ });
+ } else if (current.type === 'PLINE') {
+ const [type, mode, code, ...coords] = current.commands[0];
+ cmdStore.begin({
+ ...current,
+ commands: [
+ [type, mode, code, ...coords.slice(0, -1), [...pos], [...pos]],
+ ],
+ });
} else {
- throw new Error(`can't append to existing command ${current[0]}`);
+ throw new Error(`can't append to existing command ${current.type}`);
}
}
},
@@ -173,22 +228,23 @@ export function useInputMachine(
if (toolbarStore.selectedTool === 'line') {
e.preventDefault();
const current = cmdStore.current;
- if (current === null || current[0] !== 'PLINE') return;
+ if (current === null || current.type !== 'PLINE') return;
- const [id, mode, code, ...coords] = current;
+ const [type, mode, code, ...coords] = current.commands[0];
if (coords.length < 3) {
cmdStore.abort();
} else {
- cmdStore.current = [id, mode, code, ...coords.slice(0, -1)];
- cmdStore.commit();
+ picStore.selection = cmdStore.commit({
+ ...current,
+ commands: [[type, mode, code, ...coords.slice(0, -1)]],
+ });
}
-
return;
}
},
wheel: (e: WheelEvent) => {
- const stage = unref(stageRef);
+ const stage = unref(canvasRef);
if (!stage) return;
const [sWidth, sHeight] = unref(stageResRef);
const [dx, dy] = [e.offsetX - sWidth / 2, e.offsetY - sHeight / 2];
@@ -228,9 +284,8 @@ export function useInputMachine(
};
onMounted(() => {
- const el = unref(stageRef);
+ const el = unref(canvasRef);
if (!el) return;
-
el.addEventListener('click', mouseHandlers.click);
el.addEventListener('contextmenu', mouseHandlers.contextMenu);
el.addEventListener('wheel', mouseHandlers.wheel);
@@ -238,4 +293,15 @@ export function useInputMachine(
el.addEventListener('pointermove', pointerHandlers.move);
el.addEventListener('pointerup', pointerHandlers.up);
});
+
+ onUnmounted(() => {
+ const el = unref(canvasRef);
+ if (!el) return;
+ el.removeEventListener('pointerup', pointerHandlers.up);
+ el.removeEventListener('pointermove', pointerHandlers.move);
+ el.removeEventListener('pointerdown', pointerHandlers.down);
+ el.removeEventListener('wheel', mouseHandlers.wheel);
+ el.removeEventListener('contextmenu', mouseHandlers.contextMenu);
+ el.removeEventListener('click', mouseHandlers.click);
+ });
}
diff --git a/apps/scishop/src/data/paletteStore.ts b/apps/scishop/src/data/paletteStore.ts
new file mode 100644
index 0000000..eebabec
--- /dev/null
+++ b/apps/scishop/src/data/paletteStore.ts
@@ -0,0 +1,94 @@
+import { computed, ref, unref } from 'vue';
+
+import {
+ DrawCodes,
+ DrawMode,
+ isControlMode,
+ isPriorityMode,
+ isVisualMode,
+} from '@4bitlabs/sci0';
+import { IBM5153Contrast, Palettes } from '@4bitlabs/color';
+import { DEFAULT_PALETTE, mapToPals } from '../helpers/palette-helpers.ts';
+import store from './picStore.ts';
+
+export const drawState = ref<[DrawMode, ...DrawCodes]>([
+ DrawMode.Visual | DrawMode.Priority,
+ 0,
+ 0,
+ 0,
+]);
+
+const changeDrawMode = (mode: DrawMode, flag: DrawMode, enabled: boolean) =>
+ enabled ? mode | flag : mode & ~flag;
+
+export const visualEnabled = computed({
+ get: () => isVisualMode(unref(drawState)[0]),
+ set(next) {
+ drawState.value[0] = changeDrawMode(
+ unref(drawState)[0],
+ DrawMode.Visual,
+ next,
+ );
+ },
+});
+export const visualCode = computed({
+ get: () => unref(drawState)[1],
+ set(next) {
+ drawState.value[0] |= DrawMode.Visual;
+ drawState.value[1] = next;
+ },
+});
+
+export const priorityEnabled = computed({
+ get: () => isPriorityMode(unref(drawState)[0]),
+ set(next) {
+ drawState.value[0] = changeDrawMode(
+ unref(drawState)[0],
+ DrawMode.Priority,
+ next,
+ );
+ },
+});
+export const priorityCode = computed({
+ get: () => unref(drawState)[2],
+ set(next) {
+ drawState.value[0] |= DrawMode.Control;
+ drawState.value[2] = next;
+ },
+});
+
+export const controlEnabled = computed({
+ get: () => isControlMode(unref(drawState)[0]),
+ set(next) {
+ drawState.value[0] = changeDrawMode(
+ unref(drawState)[0],
+ DrawMode.Control,
+ next,
+ );
+ },
+});
+export const controlCode = computed({
+ get: () => unref(drawState)[3],
+ set(next) {
+ drawState.value[0] |= DrawMode.Control;
+ drawState.value[3] = next;
+ },
+});
+
+export const selectedPaletteRef = ref<0 | 1 | 2 | 3>(0);
+export const baseScreenPalette = ref(Palettes.TRUE_CGA_PALETTE);
+export const screenPalette = computed(() =>
+ IBM5153Contrast(unref(baseScreenPalette), 0.4),
+);
+
+export const paletteSetStack = computed(() => mapToPals(store.layers));
+
+export const topPaletteSet = computed(() => {
+ return store.topIdx > 0
+ ? unref(paletteSetStack)[store.topIdx - 1]
+ : [DEFAULT_PALETTE, DEFAULT_PALETTE, DEFAULT_PALETTE, DEFAULT_PALETTE];
+});
+
+export const palette = computed(
+ () => unref(topPaletteSet)[unref(selectedPaletteRef)],
+);
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
index 4484e66..eb408f9 100644
--- a/apps/scishop/src/data/picStore.ts
+++ b/apps/scishop/src/data/picStore.ts
@@ -1,4 +1,4 @@
-import { computed, ref, shallowRef, unref } from 'vue';
+import { ref, shallowRef, unref } from 'vue';
import { DrawCommand } from '@4bitlabs/sci0';
import { insert } from '../helpers/array-helpers.ts';
@@ -6,25 +6,26 @@ import { EditorCommand } from '../models/EditorCommand.ts';
const nextId = () => Math.random().toString(36).substring(2);
-const data: EditorCommand[] = [];
+const wrapRawCommand = (cmd: DrawCommand): EditorCommand =>
+ ({ id: nextId(), type: cmd[0], commands: [cmd] }) as EditorCommand;
+const refData: DrawCommand[] = [];
+
+const data: EditorCommand[] = refData.map(wrapRawCommand);
const layersRef = shallowRef(data);
-const commandsRef = computed(() =>
- unref(layersRef).flatMap((it) => it.commands),
-);
const selectedCommandIdx = ref(null);
-const topIdxRef = ref(data.length - 1);
+const topIdxRef = ref(data.length);
export default {
get layers() {
return unref(layersRef);
},
- get commands() {
- return unref(commandsRef);
- },
- get cmdIdx() {
+ get selection() {
return unref(selectedCommandIdx);
},
+ set selection(it: number | null) {
+ selectedCommandIdx.value = it;
+ },
get topIdx() {
return unref(topIdxRef);
},
@@ -33,29 +34,25 @@ export default {
},
};
-const currentCommandRef = shallowRef(null);
+const currentCommandRef = shallowRef(null);
export const currentCommandStore = {
get current() {
return unref(currentCommandRef);
},
- set current(cmd: DrawCommand | null) {
- currentCommandRef.value = cmd;
- },
get commands() {
const cmd = unref(currentCommandRef);
- return cmd ? [cmd] : [];
+ return cmd ? [...cmd.commands] : [];
},
- commit() {
- const cmd = unref(currentCommandRef);
- if (cmd === null) return;
+ begin(cmd: EditorCommand) {
+ currentCommandRef.value = cmd;
+ },
+ commit(cmd: EditorCommand): number {
currentCommandRef.value = null;
- layersRef.value = insert(layersRef.value, unref(topIdxRef) + 1, {
- id: nextId(),
- type: cmd[0],
- commands: [cmd],
- });
+ const insertPosition = unref(topIdxRef);
+ layersRef.value = insert(layersRef.value, insertPosition, cmd);
topIdxRef.value += 1;
+ return insertPosition;
},
abort() {
currentCommandRef.value = null;
diff --git a/apps/scishop/src/helpers/getPals.ts b/apps/scishop/src/helpers/palette-helpers.ts
similarity index 77%
rename from apps/scishop/src/helpers/getPals.ts
rename to apps/scishop/src/helpers/palette-helpers.ts
index cc7a7fd..87782a6 100644
--- a/apps/scishop/src/helpers/getPals.ts
+++ b/apps/scishop/src/helpers/palette-helpers.ts
@@ -12,13 +12,11 @@ type PaletteSet = [number[], number[], number[], number[]];
export const mapToPals = (commands: EditorCommand[]) => {
return commands.reduce(
- (stack: PaletteSet[], editorCmd: EditorCommand) => {
- const prevSet = stack[stack.length - 1] ?? [
- DEFAULT_PALETTE,
- DEFAULT_PALETTE,
- DEFAULT_PALETTE,
- DEFAULT_PALETTE,
- ];
+ (stack: PaletteSet[], editorCmd: EditorCommand, stackIdx: number) => {
+ const prevSet: PaletteSet =
+ stackIdx === 0
+ ? [DEFAULT_PALETTE, DEFAULT_PALETTE, DEFAULT_PALETTE, DEFAULT_PALETTE]
+ : stack[stack.length - 1];
const { type } = editorCmd;
switch (type) {
@@ -30,10 +28,10 @@ export const mapToPals = (commands: EditorCommand[]) => {
}
case 'UPDATE_PALETTE': {
const [, entries] = editorCmd.commands[0];
- const next = entries.reduce((pals, [palIdx, idx, color]) => {
+ const next = entries.reduce((pals, [palIdx, clrIdx, color]) => {
const nextSet: PaletteSet = [...prevSet];
const nextPal = [...nextSet[palIdx]];
- nextPal[idx] = color;
+ nextPal[clrIdx] = color;
nextSet[palIdx] = nextPal;
return pals;
}, prevSet);
diff --git a/apps/scishop/src/helpers/vec2-helpers.ts b/apps/scishop/src/helpers/vec2-helpers.ts
index cd8ac12..e2aa98d 100644
--- a/apps/scishop/src/helpers/vec2-helpers.ts
+++ b/apps/scishop/src/helpers/vec2-helpers.ts
@@ -1,7 +1,6 @@
export type Vec2 = [number, number];
-export type StaticVec2 = readonly [number, number];
export const intVec2 = (
- [x, y]: StaticVec2,
+ [x, y]: Readonly,
fn: (v: number) => number = Math.floor,
-): StaticVec2 => [fn(x), fn(y)];
+): Vec2 => [fn(x), fn(y)];
diff --git a/apps/scishop/src/models/EditorCommand.ts b/apps/scishop/src/models/EditorCommand.ts
index f057cc4..5d8639a 100644
--- a/apps/scishop/src/models/EditorCommand.ts
+++ b/apps/scishop/src/models/EditorCommand.ts
@@ -12,14 +12,14 @@ export type BasicEditorCommand = {
readonly id: symbol | number | string;
readonly type: T[0];
readonly name?: string;
- readonly commands: readonly [T];
+ readonly commands: [T];
};
export type CompositeEditorCommand = {
readonly id: symbol | number | string;
readonly type: T;
readonly name?: string;
- readonly commands: readonly (PolylineCommand | FillCommand | BrushCommand)[];
+ readonly commands: (PolylineCommand | FillCommand | BrushCommand)[];
};
export type GroupEditorCommand = CompositeEditorCommand<'group'>;
diff --git a/apps/scishop/src/render/cursor-dot.ts b/apps/scishop/src/render/cursor-dot.ts
new file mode 100644
index 0000000..25c9cff
--- /dev/null
+++ b/apps/scishop/src/render/cursor-dot.ts
@@ -0,0 +1,14 @@
+import { Vec2 } from '../helpers/vec2-helpers.ts';
+
+export function cursorDot(
+ ctx: CanvasRenderingContext2D,
+ [x, y]: Vec2,
+ size = 2,
+) {
+ ctx.save();
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fillStyle = 'white';
+ ctx.fill();
+ ctx.restore();
+}
\ No newline at end of file
diff --git a/apps/scishop/src/render/fill-skeleton.ts b/apps/scishop/src/render/fill-skeleton.ts
new file mode 100644
index 0000000..b5b01b4
--- /dev/null
+++ b/apps/scishop/src/render/fill-skeleton.ts
@@ -0,0 +1,28 @@
+import { applyToPoints, Matrix } from 'transformation-matrix';
+
+import { FillCommand } from '@4bitlabs/sci0';
+
+export function fillSkeleton(
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ matrix: Matrix,
+ cmd: FillCommand,
+) {
+ const [, , , ...points] = cmd;
+ const all = applyToPoints(
+ matrix,
+ points.map(([x, y]) => [x + 0.5, y + 0.5]),
+ );
+
+ all.forEach(([x, y]) => {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.beginPath();
+ ctx.moveTo(3, 0);
+ ctx.lineTo(0, -4);
+ ctx.lineTo(-3, 0);
+ ctx.lineTo(0, 4);
+ ctx.lineTo(3, 0);
+ ctx.stroke();
+ ctx.restore();
+ });
+}
diff --git a/apps/scishop/src/render/pixel-border.ts b/apps/scishop/src/render/pixel-border.ts
new file mode 100644
index 0000000..3100aa9
--- /dev/null
+++ b/apps/scishop/src/render/pixel-border.ts
@@ -0,0 +1,21 @@
+import { Vec2 } from '../helpers/vec2-helpers.ts';
+import { areaOfPolygon, pathPoly } from '../helpers/polygons.ts';
+import * as SmoothStep from '../helpers/smoothstep.ts';
+
+export function pixelBorder(ctx: CanvasRenderingContext2D, points: Vec2[]) {
+ ctx.save();
+ pathPoly(ctx, points);
+
+ ctx.lineWidth = 1;
+ ctx.setLineDash([1, 1]);
+ ctx.strokeStyle = 'white';
+
+ const area = areaOfPolygon(points);
+ const alpha = SmoothStep.s1(1, 6 * 6, area);
+ ctx.strokeStyle = `oklab(${(alpha * 100).toFixed(0)}% 0 0)`;
+ ctx.fillStyle = `oklab(${(alpha * 100 * 0.45).toFixed(0)}% 0 0)`;
+
+ ctx.fill();
+ ctx.stroke();
+ ctx.restore();
+}
diff --git a/apps/scishop/src/render/pline-skeleton.ts b/apps/scishop/src/render/pline-skeleton.ts
new file mode 100644
index 0000000..5ae2a31
--- /dev/null
+++ b/apps/scishop/src/render/pline-skeleton.ts
@@ -0,0 +1,32 @@
+import { applyToPoints, Matrix } from 'transformation-matrix';
+
+import { PolylineCommand } from '@4bitlabs/sci0';
+
+export function plineSkeleton(
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ matrix: Matrix,
+ cmd: PolylineCommand,
+) {
+ ctx.save();
+
+ const [, , , ...cPoints] = cmd;
+ const points = applyToPoints(
+ matrix,
+ cPoints.map(([x, y]) => [x + 0.5, y + 0.5]),
+ );
+
+ ctx.beginPath();
+ points.forEach(([x, y], idx) => ctx[idx ? 'lineTo' : 'moveTo'](x, y));
+ ctx.stroke();
+
+ points.forEach(([x, y]) => {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.beginPath();
+ ctx.roundRect(-3, -3, 6, 6, 0.5);
+ ctx.stroke();
+ ctx.restore();
+ });
+
+ ctx.restore();
+}
diff --git a/apps/scishop/src/reset.css b/apps/scishop/src/reset.css
index f4e1fb2..668f8b4 100644
--- a/apps/scishop/src/reset.css
+++ b/apps/scishop/src/reset.css
@@ -26,7 +26,7 @@ footer, header, hgroup, menu, nav, section {
body {
line-height: 1;
}
-ol, ul {
+ol, ul, menu {
list-style: none;
}
blockquote, q {
@@ -41,6 +41,7 @@ table {
border-collapse: collapse;
border-spacing: 0;
}
+
button {
font: inherit;
text-rendering: inherit;
@@ -56,4 +57,5 @@ button {
text-indent: inherit;
text-shadow: inherit;
text-align: inherit;
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/apps/scishop/src/style.css b/apps/scishop/src/style.css
index 8804fd8..11f4291 100644
--- a/apps/scishop/src/style.css
+++ b/apps/scishop/src/style.css
@@ -41,11 +41,22 @@
--clr-surface--default: #f5f5f5;
- --clr-surface--shade: color-mix(in srgb, var(--clr-ink), var(--clr-surface--default) 85%);
--clr-surface--glass: #2f2f2f;
+ --clr-surface: var(--clr-surface--default);
+ --clr-surface-800: color-mix(in srgb, var(--clr-surface--default), var(--clr-surface--glass) 80%);
+ --clr-surface-500: color-mix(in srgb, var(--clr-surface--default), var(--clr-surface--glass) 50%);
+ --clr-surface-200: color-mix(in srgb, var(--clr-surface--default), var(--clr-surface--glass) 20%);
--radius: 0.25rlh;
--shadow: 0 0.5rlh 0.5rlh rgba(0 0 0 / 20%);
+
+ --transparent-img: repeating-linear-gradient(
+ -45deg,
+ rgba(0 0 0 / 5%) 0px,
+ rgba(0 0 0 / 5%) 4px,
+ rgba(0 0 0 / 0%) 4px,
+ rgba(0 0 0 / 0%) 8px
+ );
}
#app {
@@ -56,7 +67,7 @@
display: grid;
grid-template-rows: 3rem 1rem calc(100vh - 6.5rem) 1rem 1.5rem;
- grid-template-columns: 1rem 3rem 1fr minmax(13rem, 15cqw);
+ grid-template-columns: 1rem 3rem 1fr 19rem;
overflow: hidden;
grid-template-areas:
diff --git a/apps/scishop/tsconfig.json b/apps/scishop/tsconfig.json
index 9e03e60..3d9a264 100644
--- a/apps/scishop/tsconfig.json
+++ b/apps/scishop/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
From dddedc5b1a7ce0ee8d483aa7a85921a1f86de44a Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Tue, 28 May 2024 10:29:28 -0600
Subject: [PATCH 11/28] WIP: add path editing.
---
apps/scishop/src/components/Sidebar.vue | 1 +
.../src/composables/useInputMachine.ts | 163 ++++++++++++------
apps/scishop/src/data/picStore.ts | 2 +-
apps/scishop/src/helpers/command-helpers.ts | 52 ++++++
apps/scishop/src/helpers/vec2-helpers.ts | 13 +-
apps/scishop/src/models/EditorCommand.ts | 18 +-
apps/scishop/src/render/cursor-dot.ts | 9 +-
7 files changed, 197 insertions(+), 61 deletions(-)
create mode 100644 apps/scishop/src/helpers/command-helpers.ts
diff --git a/apps/scishop/src/components/Sidebar.vue b/apps/scishop/src/components/Sidebar.vue
index d3d20a9..021e89f 100644
--- a/apps/scishop/src/components/Sidebar.vue
+++ b/apps/scishop/src/components/Sidebar.vue
@@ -19,5 +19,6 @@ import PalettePicker from './PalettePicker';
background-color: var(--clr-surface-200);
gap: 0.25lh;
padding-block: 0.25lh;
+ padding-left: 0.25lh;
}
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index eb15368..923d812 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -2,7 +2,6 @@ import {
Ref,
computed,
onMounted,
- ref,
shallowRef,
unref,
watch,
@@ -20,6 +19,7 @@ import {
translate,
} from 'transformation-matrix';
+import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
import toolbarStore from '../data/toolbarStore';
import viewStore from '../data/viewStore';
import { get2dContext } from '../helpers/getContext.ts';
@@ -28,13 +28,21 @@ import { useRafRef } from './useRafRef.ts';
import picStore, {
currentCommandStore,
currentCommandStore as cmdStore,
+ layersRef,
} from '../data/picStore.ts';
-import { intVec2 } from '../helpers/vec2-helpers.ts';
+import { toInt, isEqual } from '../helpers/vec2-helpers.ts';
import { drawState } from '../data/paletteStore.ts';
import { pixelBorder } from '../render/pixel-border.ts';
import { fillSkeleton } from '../render/fill-skeleton.ts';
import { plineSkeleton } from '../render/pline-skeleton.ts';
import { cursorDot } from '../render/cursor-dot.ts';
+import {
+ findClosestPoint,
+ moveFillVertex,
+ moveLineVertex,
+} from '../helpers/command-helpers.ts';
+import { insert } from '../helpers/array-helpers.ts';
+import { BasicEditorCommand } from '../models/EditorCommand.ts';
const clampZoom = (current: number, next: number, min: number, max: number) => {
if (current * next < min) return min / current;
@@ -42,7 +50,16 @@ const clampZoom = (current: number, next: number, min: number, max: number) => {
return next;
};
-type CSSCursor = string;
+// TODO collapse
+type ViewDragState = ['view', Matrix, number, number];
+type PointDragState = [
+ 'point',
+ layerIdx: number,
+ cmdIdx: number,
+ vertexIdx: number,
+ ix: number,
+ iy: number,
+];
export function useInputMachine(
matrixRef: Ref,
@@ -51,8 +68,7 @@ export function useInputMachine(
canvasResRef: Ref<[number, number]>,
) {
const iMatrixRef = computed(() => inverse(unref(matrixRef)));
- const dragStateRef = shallowRef<[Matrix, number, number] | null>(null);
- const currentCursorRef = ref('auto');
+ const dragStateRef = shallowRef(null);
const selectedLayerRef = computed(() => {
const current = unref(currentCommandStore.current);
@@ -62,45 +78,85 @@ export function useInputMachine(
});
const cursorPositionRef = useRafRef<[number, number]>([0, 0]);
- const canvasPositionRef = computed<[number, number]>(() => {
- const sPos = unref(cursorPositionRef);
- const iMatrix = unref(iMatrixRef);
- return applyToPoint(iMatrix, sPos);
- });
+ const canvasPositionRef = computed<[number, number]>(() =>
+ applyToPoint(unref(iMatrixRef), unref(cursorPositionRef)),
+ );
const canvasPixelRef = computed<[number, number]>((prev) => {
- const next = intVec2(unref(canvasPositionRef));
- const isSame = prev && prev[0] === next[0] && prev[1] === next[1];
+ const next = toInt(unref(canvasPositionRef));
+ const isSame = prev && isEqual(prev, next);
return isSame ? prev : next;
});
const isOverCanvasRef = computed(() => {
- const canvasPoint = unref(canvasPositionRef);
+ const canvasPoint = unref(canvasPixelRef);
const [cWidth, cHeight] = unref(canvasResRef);
return isInsideBounds([cWidth, cHeight], canvasPoint);
});
- const isCursorHiddenRef = computed(() => {
- const isTool = ['line', 'fill'].includes(toolbarStore.selectedTool);
- const isOverCanvas = unref(isOverCanvasRef);
- return isTool && isOverCanvas;
- });
-
watchEffect(() => {
const el = unref(canvasRef);
if (!el) return;
- const hidden = unref(isCursorHiddenRef);
- const currentCursor = unref(currentCursorRef);
- el.style.cursor = hidden ? 'none' : currentCursor;
+
+ const dragState = unref(dragStateRef);
+ const { selectedTool } = toolbarStore;
+
+ let currentCursor = 'auto';
+ if (dragState !== null && dragState[0] === 'view') {
+ currentCursor = 'grabbing';
+ } else if (selectedTool === 'pan') {
+ currentCursor = 'grab';
+ } else if (selectedTool === 'select') {
+ currentCursor = 'crosshair';
+ } else if (['line', 'fill'].includes(selectedTool)) {
+ const isOverCanvas = unref(isOverCanvasRef);
+ if (isOverCanvas) currentCursor = 'none';
+ }
+
+ el.style.cursor = currentCursor;
});
+ // Apply current pan state
watch([cursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
if (!dragState) return;
- const [matrix, ix, iy] = dragState;
+ const [mode] = dragState;
+ if (mode !== 'view') return;
+
+ const [, matrix, ix, iy] = dragState;
const dx = ix - cX;
const dy = iy - cY;
viewStore.viewMatrix = compose(translate(-dx, -dy), matrix);
});
+ // Apply point drag state
+ watch([canvasPixelRef, dragStateRef], ([pos, dragState]) => {
+ if (!dragState) return;
+ const [mode] = dragState;
+ if (mode !== 'point') return;
+
+ const [, lIdx, cIdx, pIdx] = dragState;
+ const layer = unref(picStore.layers)[lIdx];
+ if (layer.type === 'PLINE') {
+ const cmd = layer.commands[cIdx];
+ const next: BasicEditorCommand = {
+ ...layer,
+ commands: [moveLineVertex(cmd, pIdx, pos)],
+ };
+ layersRef.value = insert(picStore.layers, lIdx, next, true);
+ return;
+ }
+
+ if (layer.type === 'FILL') {
+ const cmd = layer.commands[cIdx];
+ const next: BasicEditorCommand = {
+ ...layer,
+ commands: [moveFillVertex(cmd, pos)],
+ };
+ layersRef.value = insert(picStore.layers, lIdx, next, true);
+ return;
+ }
+ });
+
+ // Update the UI canvas
watch(
[
canvasRef,
@@ -167,28 +223,10 @@ export function useInputMachine(
}
});
- watchEffect(() => {
- const el = unref(canvasRef);
- if (!el) return;
-
- const stateState = unref(dragStateRef);
- const { selectedTool } = toolbarStore;
-
- if (stateState !== null) {
- currentCursorRef.value = 'grabbing';
- } else if (selectedTool === 'pan') {
- currentCursorRef.value = 'grab';
- } else if (selectedTool === 'select') {
- currentCursorRef.value = 'crosshair';
- } else {
- currentCursorRef.value = 'auto';
- }
- });
-
const mouseHandlers = {
click: (e: MouseEvent) => {
if (toolbarStore.selectedTool === 'fill') {
- const pos = intVec2(
+ const pos = toInt(
applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
);
const [drawMode, ...drawCodes] = unref(drawState);
@@ -197,8 +235,11 @@ export function useInputMachine(
type: 'FILL',
commands: [['FILL', drawMode, drawCodes, pos]],
});
- } else if (toolbarStore.selectedTool === 'line') {
- const pos = intVec2(
+ return;
+ }
+
+ if (toolbarStore.selectedTool === 'line') {
+ const pos = toInt(
applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
);
@@ -266,19 +307,43 @@ export function useInputMachine(
const pointerHandlers = {
down: (e: PointerEvent) => {
- const isPanning =
- e.button === 1 ||
- (toolbarStore.selectedTool === 'pan' && e.button === 0);
+ if (dragStateRef.value !== null) {
+ // already panning
+ return;
+ }
+ const { selectedTool } = toolbarStore;
+ const isPanning =
+ (selectedTool === 'pan' && e.button === 0) || e.button === 1;
if (isPanning) {
- dragStateRef.value = [viewStore.viewMatrix, e.offsetX, e.offsetY];
+ dragStateRef.value = [
+ 'view',
+ viewStore.viewMatrix,
+ e.offsetX,
+ e.offsetY,
+ ];
+ return;
+ }
+
+ if (selectedTool === 'select' && e.button === 0) {
+ // check if is within radius of any point of the selected layer
+ const selIdx = unref(picStore.selection);
+ if (selIdx === null) return;
+
+ const layer = unref(picStore.layers)[selIdx];
+ if (!layer) return;
+
+ const cPos = unref(canvasPositionRef);
+ const found = findClosestPoint(layer, cPos, 5 / viewStore.zoom);
+ if (!found) return;
+ const [cIdx, pIdx, ix, iy] = found;
+ dragStateRef.value = ['point', selIdx, cIdx, pIdx, ix, iy];
}
},
move: (e: PointerEvent) => {
cursorPositionRef.value = [e.offsetX, e.offsetY];
},
up: () => {
- if (!dragStateRef.value) return;
dragStateRef.value = null;
},
};
diff --git a/apps/scishop/src/data/picStore.ts b/apps/scishop/src/data/picStore.ts
index eb408f9..e8c8327 100644
--- a/apps/scishop/src/data/picStore.ts
+++ b/apps/scishop/src/data/picStore.ts
@@ -12,7 +12,7 @@ const wrapRawCommand = (cmd: DrawCommand): EditorCommand =>
const refData: DrawCommand[] = [];
const data: EditorCommand[] = refData.map(wrapRawCommand);
-const layersRef = shallowRef(data);
+export const layersRef = shallowRef(data);
const selectedCommandIdx = ref(null);
const topIdxRef = ref(data.length);
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
new file mode 100644
index 0000000..53228c2
--- /dev/null
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -0,0 +1,52 @@
+import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
+import { EditorCommand } from '../models/EditorCommand.ts';
+import { distanceSquared, Vec2 } from './vec2-helpers.ts';
+import { insert } from './array-helpers.ts';
+
+export type FindResult = [cIdx: number, pIdx: number, x: number, y: number];
+
+type FindState = [d: number, cIdx: number, pIdx: number, x: number, y: number];
+
+export function findClosestPoint(
+ layer: EditorCommand,
+ position: Vec2,
+ range: number,
+): FindResult | null {
+ const result = layer.commands.reduce(
+ ($state, cmd, cmdIdx) => {
+ const [type] = cmd;
+ if (type !== 'PLINE' && type !== 'FILL') return $state;
+ const [, , , ...coords] = cmd;
+ return coords.reduce((state, [x, y], pIdx) => {
+ const [pDist2, ,] = state;
+ const dist2 = distanceSquared(position, [x + 0.5, y + 0.5]);
+ return dist2 < pDist2 ? [dist2, cmdIdx, pIdx, x, y] : state;
+ }, $state);
+ },
+ [Infinity, NaN, NaN, NaN, NaN],
+ );
+
+ const [distSqrd, ...rest] = result;
+ if (!Number.isFinite(distSqrd)) return null;
+
+ const dist = Math.sqrt(distSqrd);
+ if (dist > range) return null;
+ return rest;
+}
+
+export function moveLineVertex(
+ source: PolylineCommand,
+ idx: number,
+ pos: [number, number],
+): PolylineCommand {
+ const [type, mode, codes, ...verts] = source;
+ return [type, mode, codes, ...insert(verts, idx, pos, true)];
+}
+
+export function moveFillVertex(
+ source: FillCommand,
+ pos: [number, number],
+): FillCommand {
+ const [type, mode, codes] = source;
+ return [type, mode, codes, pos];
+}
diff --git a/apps/scishop/src/helpers/vec2-helpers.ts b/apps/scishop/src/helpers/vec2-helpers.ts
index e2aa98d..2f9c9ed 100644
--- a/apps/scishop/src/helpers/vec2-helpers.ts
+++ b/apps/scishop/src/helpers/vec2-helpers.ts
@@ -1,6 +1,17 @@
export type Vec2 = [number, number];
-export const intVec2 = (
+export const toInt = (
[x, y]: Readonly,
fn: (v: number) => number = Math.floor,
): Vec2 => [fn(x), fn(y)];
+
+export const isEqual = (a: Vec2, b: Vec2): boolean =>
+ a[0] === b[0] && a[1] === b[1];
+
+export const distanceSquared = (a: Vec2, b: Vec2) => {
+ const dx = a[0] - b[0];
+ const dy = a[1] - b[1];
+ return dx ** 2 + dy ** 2;
+};
+export const distanceBetween = (a: Vec2, b: Vec2) =>
+ Math.sqrt(distanceSquared(a, b));
diff --git a/apps/scishop/src/models/EditorCommand.ts b/apps/scishop/src/models/EditorCommand.ts
index 5d8639a..9a461c0 100644
--- a/apps/scishop/src/models/EditorCommand.ts
+++ b/apps/scishop/src/models/EditorCommand.ts
@@ -15,17 +15,17 @@ export type BasicEditorCommand = {
readonly commands: [T];
};
-export type CompositeEditorCommand = {
- readonly id: symbol | number | string;
- readonly type: T;
- readonly name?: string;
- readonly commands: (PolylineCommand | FillCommand | BrushCommand)[];
-};
-
-export type GroupEditorCommand = CompositeEditorCommand<'group'>;
+// export type CompositeEditorCommand = {
+// readonly id: symbol | number | string;
+// readonly type: T;
+// readonly name?: string;
+// readonly commands: (PolylineCommand | FillCommand | BrushCommand)[];
+// };
+//
+// export type GroupEditorCommand = CompositeEditorCommand<'group'>;
export type EditorCommand =
- | GroupEditorCommand
+ // | GroupEditorCommand
| BasicEditorCommand
| BasicEditorCommand
| BasicEditorCommand
diff --git a/apps/scishop/src/render/cursor-dot.ts b/apps/scishop/src/render/cursor-dot.ts
index 25c9cff..468186a 100644
--- a/apps/scishop/src/render/cursor-dot.ts
+++ b/apps/scishop/src/render/cursor-dot.ts
@@ -6,9 +6,16 @@ export function cursorDot(
size = 2,
) {
ctx.save();
+
+ ctx.beginPath();
+ ctx.arc(x, y, size * 1.5, 0, Math.PI * 2);
+ ctx.fillStyle = 'black';
+ ctx.fill();
+
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
+
ctx.restore();
-}
\ No newline at end of file
+}
From 4b230f239cfed2e2c5ae4dc52dbe4a3c575ecbc5 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Tue, 28 May 2024 17:53:03 -0600
Subject: [PATCH 12/28] WIP: extract vec2 helpers into @4bitlabs/vec2
---
apps/scishop/package.json | 5 +++--
apps/scishop/src/components/Header.vue | 1 -
apps/scishop/src/components/Sidebar.vue | 4 ++--
apps/scishop/src/components/Stage.vue | 4 ++--
.../src/composables/useCanvasRenderer.ts | 11 +++++------
.../src/composables/useInputMachine.ts | 19 ++++++++++++++-----
apps/scishop/src/helpers/command-helpers.ts | 6 +++---
apps/scishop/src/helpers/polygons.ts | 15 +++++++--------
apps/scishop/src/helpers/vec2-helpers.ts | 17 -----------------
apps/scishop/src/render/cursor-dot.ts | 4 ++--
apps/scishop/src/render/pixel-border.ts | 7 +++++--
package-lock.json | 10 ++++++----
12 files changed, 49 insertions(+), 54 deletions(-)
delete mode 100644 apps/scishop/src/helpers/vec2-helpers.ts
diff --git a/apps/scishop/package.json b/apps/scishop/package.json
index 26b28e0..9bcc431 100644
--- a/apps/scishop/package.json
+++ b/apps/scishop/package.json
@@ -10,9 +10,10 @@
},
"dependencies": {
"@4bitlabs/sci0": "^2.7.0",
- "@4bitlabs/color": "^2.1.3",
+ "@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
- "@4bitlabs/image": "^3.3.8",
+ "@4bitlabs/image": "^3.3.9",
+ "@4bitlabs/vec2": "^1.0.1",
"vue": "^3.4.21",
"transformation-matrix": "^2.16.1"
},
diff --git a/apps/scishop/src/components/Header.vue b/apps/scishop/src/components/Header.vue
index 0e4ba84..a74ef64 100644
--- a/apps/scishop/src/components/Header.vue
+++ b/apps/scishop/src/components/Header.vue
@@ -21,7 +21,6 @@
color: var(--clr-ink);
grid-area: header;
display: flex;
- border-bottom: 1px solid var(--clr-surface-200);
align-items: center;
gap: 1ch;
padding-inline: 1ch;
diff --git a/apps/scishop/src/components/Sidebar.vue b/apps/scishop/src/components/Sidebar.vue
index 021e89f..289abe0 100644
--- a/apps/scishop/src/components/Sidebar.vue
+++ b/apps/scishop/src/components/Sidebar.vue
@@ -18,7 +18,7 @@ import PalettePicker from './PalettePicker';
color: var(--clr-ink);
background-color: var(--clr-surface-200);
gap: 0.25lh;
- padding-block: 0.25lh;
- padding-left: 0.25lh;
+ padding-block: 0.2lh;
+ padding-left: 0.2lh;
}
diff --git a/apps/scishop/src/components/Stage.vue b/apps/scishop/src/components/Stage.vue
index 2449208..b3bddc0 100644
--- a/apps/scishop/src/components/Stage.vue
+++ b/apps/scishop/src/components/Stage.vue
@@ -41,7 +41,7 @@ const matrixRef = computed(() => {
);
});
-const smootherizeRef = computed(() => viewStore.zoom < 12);
+const smootherizeRef = computed(() => viewStore.zoom < 8);
watch(
[stageRef, stageRes, pixels, stageStore.canvasRes, matrixRef, smootherizeRef],
@@ -56,7 +56,7 @@ watch(
ctx.resetTransform();
ctx.imageSmoothingEnabled = smooth;
- ctx.imageSmoothingQuality = 'high';
+ ctx.imageSmoothingQuality = 'medium';
ctx.clearRect(0, 0, sWidth, sHeight);
ctx.save();
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index 4de4656..fad0ece 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,4 +1,4 @@
-import { Ref, ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
+import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
import { RenderResult } from '@4bitlabs/sci0/dist/screen/render-result.ts';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
@@ -27,8 +27,7 @@ export function useRenderedPixels(
});
}
-const soft = ref(true);
-(window as unknown as any)['soft'] = soft;
+const isSoftRef = computed(() => viewStore.zoom >= 1);
export function useCanvasRenderer(
renderedRef: Ref,
@@ -36,8 +35,8 @@ export function useCanvasRenderer(
): Ref {
const canvasRef = shallowRef(new OffscreenCanvas(1, 1));
watch(
- [renderedRef, resRef, oversampleRef, screenPaletteRef, soft],
- ([pic, [width, height], oversample, palette, softOn]) => {
+ [renderedRef, resRef, oversampleRef, screenPaletteRef, isSoftRef],
+ ([pic, [width, height], oversample, palette, isSoft]) => {
const canvas = unref(canvasRef);
canvas.width = width * oversample[0];
canvas.height = height * oversample[1];
@@ -46,7 +45,7 @@ export function useCanvasRenderer(
dither: createDitherFilter(
generateSciDitherPairs(
palette,
- softOn ? Mixers.softMixer() : ([a, b]) => [a, b],
+ isSoft ? Mixers.softMixer() : ([a, b]) => [a, b],
),
[1, 1],
),
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index 923d812..cb07a1b 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -19,6 +19,7 @@ import {
translate,
} from 'transformation-matrix';
+import { round, isEqual, vec2 } from '@4bitlabs/vec2';
import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
import toolbarStore from '../data/toolbarStore';
import viewStore from '../data/viewStore';
@@ -30,7 +31,6 @@ import picStore, {
currentCommandStore as cmdStore,
layersRef,
} from '../data/picStore.ts';
-import { toInt, isEqual } from '../helpers/vec2-helpers.ts';
import { drawState } from '../data/paletteStore.ts';
import { pixelBorder } from '../render/pixel-border.ts';
import { fillSkeleton } from '../render/fill-skeleton.ts';
@@ -82,7 +82,7 @@ export function useInputMachine(
applyToPoint(unref(iMatrixRef), unref(cursorPositionRef)),
);
const canvasPixelRef = computed<[number, number]>((prev) => {
- const next = toInt(unref(canvasPositionRef));
+ const next = round(unref(canvasPositionRef), vec2(), Math.floor);
const isSame = prev && isEqual(prev, next);
return isSame ? prev : next;
});
@@ -226,8 +226,10 @@ export function useInputMachine(
const mouseHandlers = {
click: (e: MouseEvent) => {
if (toolbarStore.selectedTool === 'fill') {
- const pos = toInt(
+ const pos = round(
applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
+ vec2(),
+ Math.floor,
);
const [drawMode, ...drawCodes] = unref(drawState);
picStore.selection = cmdStore.commit({
@@ -239,8 +241,10 @@ export function useInputMachine(
}
if (toolbarStore.selectedTool === 'line') {
- const pos = toInt(
+ const pos = round(
applyToPoint(unref(iMatrixRef), [e.offsetX, e.offsetY]),
+ vec2(),
+ Math.floor,
);
const current = cmdStore.current;
@@ -266,7 +270,8 @@ export function useInputMachine(
},
contextMenu: (e: MouseEvent) => {
- if (toolbarStore.selectedTool === 'line') {
+ const { selectedTool } = toolbarStore;
+ if (selectedTool === 'line') {
e.preventDefault();
const current = cmdStore.current;
if (current === null || current.type !== 'PLINE') return;
@@ -282,6 +287,10 @@ export function useInputMachine(
}
return;
}
+
+ if (selectedTool === 'select') {
+ e.preventDefault();
+ }
},
wheel: (e: WheelEvent) => {
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
index 53228c2..1060a3b 100644
--- a/apps/scishop/src/helpers/command-helpers.ts
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -1,6 +1,6 @@
import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
+import { squaredDistanceBetween, Vec2 } from '@4bitlabs/vec2';
import { EditorCommand } from '../models/EditorCommand.ts';
-import { distanceSquared, Vec2 } from './vec2-helpers.ts';
import { insert } from './array-helpers.ts';
export type FindResult = [cIdx: number, pIdx: number, x: number, y: number];
@@ -9,7 +9,7 @@ type FindState = [d: number, cIdx: number, pIdx: number, x: number, y: number];
export function findClosestPoint(
layer: EditorCommand,
- position: Vec2,
+ position: Readonly,
range: number,
): FindResult | null {
const result = layer.commands.reduce(
@@ -19,7 +19,7 @@ export function findClosestPoint(
const [, , , ...coords] = cmd;
return coords.reduce((state, [x, y], pIdx) => {
const [pDist2, ,] = state;
- const dist2 = distanceSquared(position, [x + 0.5, y + 0.5]);
+ const dist2 = squaredDistanceBetween(position, [x + 0.5, y + 0.5]);
return dist2 < pDist2 ? [dist2, cmdIdx, pIdx, x, y] : state;
}, $state);
},
diff --git a/apps/scishop/src/helpers/polygons.ts b/apps/scishop/src/helpers/polygons.ts
index 052479f..6a4d71f 100644
--- a/apps/scishop/src/helpers/polygons.ts
+++ b/apps/scishop/src/helpers/polygons.ts
@@ -1,19 +1,18 @@
+import { type Vec2 } from '@4bitlabs/vec2';
+
export const isInsideBounds = (
- [width, height]: [number, number],
- [x, y]: [number, number],
+ [width, height]: Readonly,
+ [x, y]: Readonly,
): boolean => x >= 0 && y >= 0 && x < width && y < height;
-export const pixel = (
- [x, y]: [number, number],
- offset = 0.0,
-): [number, number][] => [
+export const pixel = ([x, y]: Readonly, offset = 0.0): Vec2[] => [
[Math.floor(x) + offset, Math.floor(y) + offset],
[Math.ceil(x) - offset, Math.floor(y) + offset],
[Math.ceil(x) - offset, Math.ceil(y) - offset],
[Math.floor(x) + offset, Math.ceil(y) - offset],
];
-export const areaOfPolygon = (points: [number, number][]) => {
+export const areaOfPolygon = (points: Readonly[]) => {
const { length } = points;
let area = 0;
@@ -28,7 +27,7 @@ export const areaOfPolygon = (points: [number, number][]) => {
export const pathPoly = (
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
- points: [number, number][],
+ points: Readonly[],
) => {
ctx.beginPath();
points.forEach(([x, y], i) => ctx[i === 0 ? 'moveTo' : 'lineTo'](x, y));
diff --git a/apps/scishop/src/helpers/vec2-helpers.ts b/apps/scishop/src/helpers/vec2-helpers.ts
deleted file mode 100644
index 2f9c9ed..0000000
--- a/apps/scishop/src/helpers/vec2-helpers.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export type Vec2 = [number, number];
-
-export const toInt = (
- [x, y]: Readonly,
- fn: (v: number) => number = Math.floor,
-): Vec2 => [fn(x), fn(y)];
-
-export const isEqual = (a: Vec2, b: Vec2): boolean =>
- a[0] === b[0] && a[1] === b[1];
-
-export const distanceSquared = (a: Vec2, b: Vec2) => {
- const dx = a[0] - b[0];
- const dy = a[1] - b[1];
- return dx ** 2 + dy ** 2;
-};
-export const distanceBetween = (a: Vec2, b: Vec2) =>
- Math.sqrt(distanceSquared(a, b));
diff --git a/apps/scishop/src/render/cursor-dot.ts b/apps/scishop/src/render/cursor-dot.ts
index 468186a..97e8dbf 100644
--- a/apps/scishop/src/render/cursor-dot.ts
+++ b/apps/scishop/src/render/cursor-dot.ts
@@ -1,8 +1,8 @@
-import { Vec2 } from '../helpers/vec2-helpers.ts';
+import { Vec2 } from '@4bitlabs/vec2';
export function cursorDot(
ctx: CanvasRenderingContext2D,
- [x, y]: Vec2,
+ [x, y]: Readonly,
size = 2,
) {
ctx.save();
diff --git a/apps/scishop/src/render/pixel-border.ts b/apps/scishop/src/render/pixel-border.ts
index 3100aa9..b7f3b9b 100644
--- a/apps/scishop/src/render/pixel-border.ts
+++ b/apps/scishop/src/render/pixel-border.ts
@@ -1,8 +1,11 @@
-import { Vec2 } from '../helpers/vec2-helpers.ts';
+import { type Vec2 } from '@4bitlabs/vec2';
import { areaOfPolygon, pathPoly } from '../helpers/polygons.ts';
import * as SmoothStep from '../helpers/smoothstep.ts';
-export function pixelBorder(ctx: CanvasRenderingContext2D, points: Vec2[]) {
+export function pixelBorder(
+ ctx: CanvasRenderingContext2D,
+ points: Readonly[],
+) {
ctx.save();
pathPoly(ctx, points);
diff --git a/package-lock.json b/package-lock.json
index d078e5a..0eed038 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -71,10 +71,11 @@
"apps/scishop": {
"version": "0.0.0",
"dependencies": {
- "@4bitlabs/color": "^2.1.3",
+ "@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
- "@4bitlabs/image": "^3.3.8",
+ "@4bitlabs/image": "^3.3.9",
"@4bitlabs/sci0": "^2.7.0",
+ "@4bitlabs/vec2": "^1.0.1",
"transformation-matrix": "^2.16.1",
"vue": "^3.4.21"
},
@@ -23064,10 +23065,11 @@
"scishop": {
"version": "file:apps/scishop",
"requires": {
- "@4bitlabs/color": "^2.1.3",
+ "@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
- "@4bitlabs/image": "^3.3.8",
+ "@4bitlabs/image": "^3.3.9",
"@4bitlabs/sci0": "^2.7.0",
+ "@4bitlabs/vec2": "^1.0.1",
"@vitejs/plugin-vue": "^5.0.4",
"transformation-matrix": "^2.16.1",
"typescript": "^5.2.2",
From 6c9d21f173fa6fdadfddfb072014f43f9a77ec49 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Tue, 28 May 2024 21:22:48 -0600
Subject: [PATCH 13/28] WIP: refactoring closestPoint finders
---
.../scishop/src/components/LayerNavigator.vue | 4 +-
apps/scishop/src/components/Sidebar.vue | 12 ++-
.../src/composables/useInputMachine.ts | 39 +++++---
apps/scishop/src/helpers/command-helpers.ts | 98 +++++++++++++------
apps/scishop/src/helpers/exhaustive.ts | 3 +
apps/scishop/src/render/fill-skeleton.ts | 2 +
apps/scishop/src/render/pline-skeleton.ts | 4 +-
7 files changed, 112 insertions(+), 50 deletions(-)
create mode 100644 apps/scishop/src/helpers/exhaustive.ts
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index 95b3747..2c22cf2 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -85,10 +85,10 @@ const handleClick = (e: MouseEvent, idx: number) => {
scrollbar-width: thin;
align-items: start;
align-content: start;
- flex-grow: 1;
padding-bottom: 0.5lh;
- border-bottom-left-radius: 0.5lh;
background-image: var(--transparent-img);
+ background-color: var(--clr-surface--default);
+ min-height: 15rem;
}
.head {
diff --git a/apps/scishop/src/components/Sidebar.vue b/apps/scishop/src/components/Sidebar.vue
index 289abe0..8203c01 100644
--- a/apps/scishop/src/components/Sidebar.vue
+++ b/apps/scishop/src/components/Sidebar.vue
@@ -5,8 +5,8 @@ import PalettePicker from './PalettePicker';
@@ -19,6 +19,12 @@ import PalettePicker from './PalettePicker';
background-color: var(--clr-surface-200);
gap: 0.25lh;
padding-block: 0.2lh;
- padding-left: 0.2lh;
+ padding-left: 0.25lh;
+}
+
+.panel {
+ border-top-left-radius: 0.25lh;
+ border-bottom-left-radius: 0.25lh;
+ box-shadow: 0 0.125lh 0.125lh rgba(0 0 0 /15%);
}
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index cb07a1b..8dcc74e 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -37,9 +37,9 @@ import { fillSkeleton } from '../render/fill-skeleton.ts';
import { plineSkeleton } from '../render/pline-skeleton.ts';
import { cursorDot } from '../render/cursor-dot.ts';
import {
- findClosestPoint,
moveFillVertex,
moveLineVertex,
+ nearestPointWithRange,
} from '../helpers/command-helpers.ts';
import { insert } from '../helpers/array-helpers.ts';
import { BasicEditorCommand } from '../models/EditorCommand.ts';
@@ -57,8 +57,6 @@ type PointDragState = [
layerIdx: number,
cmdIdx: number,
vertexIdx: number,
- ix: number,
- iy: number,
];
export function useInputMachine(
@@ -198,13 +196,25 @@ export function useInputMachine(
if (selectedLayer) {
ctx.save();
ctx.lineWidth = 1.5;
- selectedLayer.commands.forEach((cmd) => {
+
+ const found = nearestPointWithRange(
+ selectedLayer.commands,
+ canvasPoint,
+ Math.max(0.404, 7.5 / viewStore.zoom),
+ );
+
+ selectedLayer.commands.forEach((cmd, idx) => {
const [type] = cmd;
ctx.strokeStyle = 'white';
+ ctx.fillStyle = '#999';
if (type === 'PLINE') {
- plineSkeleton(ctx, matrix, cmd);
- } else if (type === 'FILL') {
- fillSkeleton(ctx, matrix, cmd);
+ const highlight = found?.[0] == idx ? [found[1]] : [];
+ plineSkeleton(ctx, matrix, cmd, highlight);
+ }
+
+ if (type === 'FILL') {
+ const highlight = found?.[0] === idx;
+ fillSkeleton(ctx, matrix, cmd, highlight);
}
});
}
@@ -316,10 +326,7 @@ export function useInputMachine(
const pointerHandlers = {
down: (e: PointerEvent) => {
- if (dragStateRef.value !== null) {
- // already panning
- return;
- }
+ if (dragStateRef.value !== null) return; // already panning
const { selectedTool } = toolbarStore;
const isPanning =
@@ -343,10 +350,14 @@ export function useInputMachine(
if (!layer) return;
const cPos = unref(canvasPositionRef);
- const found = findClosestPoint(layer, cPos, 5 / viewStore.zoom);
+ const found = nearestPointWithRange(
+ layer.commands,
+ cPos,
+ Math.max(0.5, 7 / viewStore.zoom),
+ );
if (!found) return;
- const [cIdx, pIdx, ix, iy] = found;
- dragStateRef.value = ['point', selIdx, cIdx, pIdx, ix, iy];
+ const [cIdx, pIdx] = found;
+ dragStateRef.value = ['point', selIdx, cIdx, pIdx];
}
},
move: (e: PointerEvent) => {
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
index 1060a3b..7085840 100644
--- a/apps/scishop/src/helpers/command-helpers.ts
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -1,52 +1,90 @@
-import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
+import {
+ BrushCommand,
+ DrawCommand,
+ FillCommand,
+ PolylineCommand,
+} from '@4bitlabs/sci0';
import { squaredDistanceBetween, Vec2 } from '@4bitlabs/vec2';
-import { EditorCommand } from '../models/EditorCommand.ts';
import { insert } from './array-helpers.ts';
+import { exhaustive } from './exhaustive.ts';
-export type FindResult = [cIdx: number, pIdx: number, x: number, y: number];
+export const extractCoordinates = (
+ cmd: PolylineCommand | FillCommand | BrushCommand,
+): Vec2[] => {
+ const [type] = cmd;
+ switch (type) {
+ case 'PLINE': {
+ const [, , , ...vertices] = cmd;
+ return vertices;
+ }
+ case 'FILL': {
+ const [, , , vertex] = cmd;
+ return [vertex];
+ }
+ case 'BRUSH': {
+ const [, , , , , vertex] = cmd;
+ return [vertex];
+ }
+ default:
+ exhaustive(`unexpected type`, type);
+ }
+};
-type FindState = [d: number, cIdx: number, pIdx: number, x: number, y: number];
+export type ClosestPointState = [dSqrd: number, idx: number];
+export const findClosestPointTo = (
+ vertices: Readonly[],
+ position: Readonly,
+ initial: ClosestPointState = [Infinity, NaN],
+): ClosestPointState =>
+ vertices.reduce((state, point, pIdx) => {
+ const [prevD2] = state;
+ const thisD2 = squaredDistanceBetween(position, point);
+ return thisD2 < prevD2 ? [thisD2, pIdx] : state;
+ }, initial);
-export function findClosestPoint(
- layer: EditorCommand,
+export type FindState = [dSqrd: number, commandIdx: number, pointIdx: number];
+export const findClosestPointIn = (
+ commands: DrawCommand[],
position: Readonly,
- range: number,
-): FindResult | null {
- const result = layer.commands.reduce(
- ($state, cmd, cmdIdx) => {
+): FindState =>
+ commands.reduce(
+ (state, cmd, cmdIdx) => {
const [type] = cmd;
- if (type !== 'PLINE' && type !== 'FILL') return $state;
- const [, , , ...coords] = cmd;
- return coords.reduce((state, [x, y], pIdx) => {
- const [pDist2, ,] = state;
- const dist2 = squaredDistanceBetween(position, [x + 0.5, y + 0.5]);
- return dist2 < pDist2 ? [dist2, cmdIdx, pIdx, x, y] : state;
- }, $state);
+ if (type !== 'PLINE' && type !== 'FILL') return state;
+ const [prevD2] = state;
+ const [nextD2, pIdx] = findClosestPointTo(
+ extractCoordinates(cmd).map(([x, y]) => [x + 0.5, y + 0.5]),
+ position,
+ );
+ return nextD2 < prevD2 ? [nextD2, cmdIdx, pIdx] : state;
},
- [Infinity, NaN, NaN, NaN, NaN],
+ [Infinity, NaN, NaN],
);
+export type FindResult = [commandIdx: number, pointIdx: number];
+export function nearestPointWithRange(
+ commands: DrawCommand[],
+ position: Readonly,
+ range: number,
+): FindResult | null {
+ const result = findClosestPointIn(commands, position);
+
const [distSqrd, ...rest] = result;
if (!Number.isFinite(distSqrd)) return null;
const dist = Math.sqrt(distSqrd);
if (dist > range) return null;
+
return rest;
}
-export function moveLineVertex(
- source: PolylineCommand,
+export const moveLineVertex = (
+ [type, mode, codes, ...verts]: PolylineCommand,
idx: number,
pos: [number, number],
-): PolylineCommand {
- const [type, mode, codes, ...verts] = source;
- return [type, mode, codes, ...insert(verts, idx, pos, true)];
-}
+): PolylineCommand => [type, mode, codes, ...insert(verts, idx, pos, true)];
-export function moveFillVertex(
- source: FillCommand,
+export const moveFillVertex = (
+ [type, mode, codes]: FillCommand,
pos: [number, number],
-): FillCommand {
- const [type, mode, codes] = source;
- return [type, mode, codes, pos];
-}
+): FillCommand => [type, mode, codes, pos];
diff --git a/apps/scishop/src/helpers/exhaustive.ts b/apps/scishop/src/helpers/exhaustive.ts
new file mode 100644
index 0000000..ae3ead0
--- /dev/null
+++ b/apps/scishop/src/helpers/exhaustive.ts
@@ -0,0 +1,3 @@
+export function exhaustive(msg: string, _: never): never {
+ throw new Error(`${msg}: ${JSON.stringify(_)}`);
+}
diff --git a/apps/scishop/src/render/fill-skeleton.ts b/apps/scishop/src/render/fill-skeleton.ts
index b5b01b4..a117299 100644
--- a/apps/scishop/src/render/fill-skeleton.ts
+++ b/apps/scishop/src/render/fill-skeleton.ts
@@ -6,6 +6,7 @@ export function fillSkeleton(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
matrix: Matrix,
cmd: FillCommand,
+ highlight: boolean,
) {
const [, , , ...points] = cmd;
const all = applyToPoints(
@@ -22,6 +23,7 @@ export function fillSkeleton(
ctx.lineTo(-3, 0);
ctx.lineTo(0, 4);
ctx.lineTo(3, 0);
+ if (highlight) ctx.fill();
ctx.stroke();
ctx.restore();
});
diff --git a/apps/scishop/src/render/pline-skeleton.ts b/apps/scishop/src/render/pline-skeleton.ts
index 5ae2a31..0c46943 100644
--- a/apps/scishop/src/render/pline-skeleton.ts
+++ b/apps/scishop/src/render/pline-skeleton.ts
@@ -6,6 +6,7 @@ export function plineSkeleton(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
matrix: Matrix,
cmd: PolylineCommand,
+ highlights: number[] = [],
) {
ctx.save();
@@ -19,12 +20,13 @@ export function plineSkeleton(
points.forEach(([x, y], idx) => ctx[idx ? 'lineTo' : 'moveTo'](x, y));
ctx.stroke();
- points.forEach(([x, y]) => {
+ points.forEach(([x, y], idx) => {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.roundRect(-3, -3, 6, 6, 0.5);
ctx.stroke();
+ if (highlights.includes(idx)) ctx.fill();
ctx.restore();
});
From 413abbec54623a65b40ea325d36c33dfbaac9f79 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Wed, 29 May 2024 13:34:58 -0600
Subject: [PATCH 14/28] WIP: more core updates/features
---
apps/scishop/package.json | 1 +
apps/scishop/src/assets/cursor-pen-minus.svg | 8 +
apps/scishop/src/assets/cursor-pen-plus.svg | 8 +
apps/scishop/src/assets/cursor-pen-star.svg | 12 +
apps/scishop/src/assets/cursor-pen.svg | 10 +
.../scishop/src/components/LayerNavigator.vue | 9 +-
apps/scishop/src/components/Stage.vue | 2 +-
.../src/components/command-items/Swatches.vue | 2 +
.../src/composables/useInputMachine.ts | 287 ++++++++++++------
.../src/composables/useResizeWatcher.ts | 8 +-
apps/scishop/src/data/picStore.ts | 23 +-
apps/scishop/src/helpers/array-helpers.ts | 5 +
apps/scishop/src/helpers/command-helpers.ts | 42 ++-
apps/scishop/src/render/fill-skeleton.ts | 1 +
apps/scishop/src/render/pline-skeleton.ts | 5 +-
package-lock.json | 8 +-
16 files changed, 331 insertions(+), 100 deletions(-)
create mode 100644 apps/scishop/src/assets/cursor-pen-minus.svg
create mode 100644 apps/scishop/src/assets/cursor-pen-plus.svg
create mode 100644 apps/scishop/src/assets/cursor-pen-star.svg
create mode 100644 apps/scishop/src/assets/cursor-pen.svg
diff --git a/apps/scishop/package.json b/apps/scishop/package.json
index 9bcc431..e50068b 100644
--- a/apps/scishop/package.json
+++ b/apps/scishop/package.json
@@ -14,6 +14,7 @@
"@4bitlabs/color-space": "^1.2.2",
"@4bitlabs/image": "^3.3.9",
"@4bitlabs/vec2": "^1.0.1",
+ "fast-deep-equal": "^3.1.3",
"vue": "^3.4.21",
"transformation-matrix": "^2.16.1"
},
diff --git a/apps/scishop/src/assets/cursor-pen-minus.svg b/apps/scishop/src/assets/cursor-pen-minus.svg
new file mode 100644
index 0000000..b858f1d
--- /dev/null
+++ b/apps/scishop/src/assets/cursor-pen-minus.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/scishop/src/assets/cursor-pen-plus.svg b/apps/scishop/src/assets/cursor-pen-plus.svg
new file mode 100644
index 0000000..ef82338
--- /dev/null
+++ b/apps/scishop/src/assets/cursor-pen-plus.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/scishop/src/assets/cursor-pen-star.svg b/apps/scishop/src/assets/cursor-pen-star.svg
new file mode 100644
index 0000000..8d34b71
--- /dev/null
+++ b/apps/scishop/src/assets/cursor-pen-star.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/apps/scishop/src/assets/cursor-pen.svg b/apps/scishop/src/assets/cursor-pen.svg
new file mode 100644
index 0000000..568bad7
--- /dev/null
+++ b/apps/scishop/src/assets/cursor-pen.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index 2c22cf2..7b72bec 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -76,14 +76,17 @@ const handleClick = (e: MouseEvent, idx: number) => {
diff --git a/apps/scishop/src/composables/useCanvasRenderer.ts b/apps/scishop/src/composables/useCanvasRenderer.ts
index fad0ece..68423b4 100644
--- a/apps/scishop/src/composables/useCanvasRenderer.ts
+++ b/apps/scishop/src/composables/useCanvasRenderer.ts
@@ -1,4 +1,4 @@
-import { Ref, watch, unref, shallowRef, triggerRef, computed } from 'vue';
+import { computed, Ref, shallowRef, triggerRef, unref, watch } from 'vue';
import { RenderResult } from '@4bitlabs/sci0/dist/screen/render-result.ts';
import { DrawCommand, renderPic } from '@4bitlabs/sci0';
@@ -8,6 +8,7 @@ import { nearestNeighbor } from '@4bitlabs/resize-filters';
import { get2dContext } from '../helpers/getContext';
import viewStore from '../data/viewStore.ts';
import { screenPalette as screenPaletteRef } from '../data/paletteStore.ts';
+import { setCanvasDimensions } from '../helpers/setCanvasDimensions.ts';
const oversampleRef = computed<[number, number]>(() => {
if (viewStore.zoom > 12) return [1, 1];
@@ -28,27 +29,33 @@ export function useRenderedPixels(
}
const isSoftRef = computed(() => viewStore.zoom >= 1);
+const ditherRef = computed(() =>
+ createDitherFilter(
+ generateSciDitherPairs(
+ unref(screenPaletteRef),
+ unref(isSoftRef) ? Mixers.softMixer() : ([a, b]) => [a, b],
+ ),
+ ),
+);
export function useCanvasRenderer(
renderedRef: Ref,
resRef: Ref<[number, number]>,
): Ref {
const canvasRef = shallowRef(new OffscreenCanvas(1, 1));
+
watch(
- [renderedRef, resRef, oversampleRef, screenPaletteRef, isSoftRef],
- ([pic, [width, height], oversample, palette, isSoft]) => {
+ [renderedRef, resRef, oversampleRef, ditherRef],
+ ([pic, [width, height], oversample, dither]) => {
const canvas = unref(canvasRef);
- canvas.width = width * oversample[0];
- canvas.height = height * oversample[1];
+ setCanvasDimensions(
+ canvas,
+ width * oversample[0],
+ height * oversample[1],
+ );
const imgData = renderPixelData(pic.visible, {
- dither: createDitherFilter(
- generateSciDitherPairs(
- palette,
- isSoft ? Mixers.softMixer() : ([a, b]) => [a, b],
- ),
- [1, 1],
- ),
+ dither,
post: [nearestNeighbor(oversample)],
}) as ImageData;
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index 6f583bc..2f733f2 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -25,7 +25,13 @@ import { FillCommand, PolylineCommand } from '@4bitlabs/sci0';
import toolbarStore from '../data/toolbarStore';
import viewStore from '../data/viewStore';
import { get2dContext } from '../helpers/getContext.ts';
-import { isInsideBounds, pixel } from '../helpers/polygons.ts';
+import {
+ isInsideBounds,
+ isInsidePolygon,
+ pathPoly,
+ pixel,
+ rect,
+} from '../helpers/polygons.ts';
import { useRafRef } from './useRafRef.ts';
import picStore, {
currentCommandStore,
@@ -37,6 +43,7 @@ import { pixelBorder } from '../render/pixel-border.ts';
import { fillSkeleton } from '../render/fill-skeleton.ts';
import { plineSkeleton } from '../render/pline-skeleton.ts';
import {
+ extractVertices,
FindResult,
moveFillVertex,
moveLineVertex,
@@ -51,6 +58,9 @@ import cursorPenSvg from '../assets/cursor-pen.svg';
import cursorPenStarSvg from '../assets/cursor-pen-star.svg';
import cursorPenPlusSvg from '../assets/cursor-pen-plus.svg';
import cursorPenMinusSvg from '../assets/cursor-pen-minus.svg';
+import { setCanvasDimensions } from '../helpers/setCanvasDimensions.ts';
+import { cursorDot } from '../render/cursor-dot.ts';
+import { pointSkeleton } from '../render/point-skeleton.ts';
const clampZoom = (current: number, next: number, min: number, max: number) => {
if (current * next < min) return min / current;
@@ -58,40 +68,56 @@ const clampZoom = (current: number, next: number, min: number, max: number) => {
return next;
};
+type SelectionEntry = [layerIdx: number, cmdIdx: number, vertexIdx: number];
+
type ViewDragState = ['view', iMatrix: Matrix, iPosition: Vec2];
type PointDragState = [
'point',
iPosition: Vec2,
- ...[layerIdx: number, cmdIdx: number, vertexIdx: number, initial: Vec2][],
+ ...[...SelectionEntry, initial: Vec2][],
];
+type SelectionDragState = ['sel-rect', start: Vec2];
+
+type DragStates = ViewDragState | PointDragState | SelectionDragState;
const selectedLayerRef = computed(() => {
- const current = unref(currentCommandStore.current);
- if (current) return current;
const selIdx = unref(picStore.selection);
return selIdx !== null ? unref(picStore.layers)[selIdx] : null;
});
+const activeLayerRef = computed(() => {
+ const current = unref(currentCommandStore.current);
+ if (current) return current;
+ return unref(selectedLayerRef);
+});
+
const pointerRadiusRef = computed(() => Math.max(0.404, 7.5 / viewStore.zoom));
export function useInputMachine(
matrixRef: Ref,
canvasRef: Ref,
+ selCanvasRef: Ref,
+ cursorCanvasRef: Ref,
stageResRef: Ref<[number, number]>,
canvasResRef: Ref<[number, number]>,
) {
const iMatrixRef = computed(() => inverse(unref(matrixRef)));
- const dragStateRef = shallowRef(null);
-
- const cursorPositionRef = useRafRef<[number, number]>([0, 0]);
- const canvasPositionRef = computed<[number, number]>((prev) => {
- const actual = applyToPoint(unref(iMatrixRef), unref(cursorPositionRef));
- const next = round(actual, vec2(), (i) => Math.round(i * 8) / 8);
- return prev && isEqual(prev, next) ? prev : next;
+ const dragStateRef = shallowRef(null);
+ const selectionStateRef = shallowRef([]);
+
+ const lastCursorPositionRef = useRafRef<[number, number]>([0, 0]);
+ const canvasPositionRef = computed<[number, number]>((prev = vec2()) => {
+ const actual = applyToPoint(
+ unref(iMatrixRef),
+ unref(lastCursorPositionRef),
+ );
+ const next = round(actual, vec2(), (i) => Math.floor(i * 8) / 8);
+ return isEqual(prev, next) ? prev : next;
});
- const canvasPixelRef = computed<[number, number]>((prev) => {
+
+ const canvasPixelRef = computed<[number, number]>((prev = vec2()) => {
const next = round(unref(canvasPositionRef), vec2(), Math.floor);
- return prev && isEqual(prev, next) ? prev : next;
+ return isEqual(prev, next) ? prev : next;
});
const isOverCanvasRef = computed(() => {
@@ -124,6 +150,7 @@ export function useInputMachine(
},
);
+ // Cursor updater
watchEffect(() => {
const el = unref(canvasRef);
if (!el) return;
@@ -142,22 +169,21 @@ export function useInputMachine(
const isOverCanvas = unref(isOverCanvasRef);
if (isOverCanvas) {
if (unref(currentCommandStore.current)) {
- currentCursor = `url(${cursorPenSvg}) 1 1, none`;
+ currentCursor = `url(${cursorPenSvg}) 0 0, none`;
} else if (unref(nearestExistingPointRef) !== null) {
- currentCursor = `url(${cursorPenMinusSvg}) 1 1, none`;
+ currentCursor = `url(${cursorPenMinusSvg}) 0 0, none`;
} else if (unref(nearestAddPointRef) !== null) {
- currentCursor = `url(${cursorPenPlusSvg}) 1 1, none`;
+ currentCursor = `url(${cursorPenPlusSvg}) 0 0, none`;
} else {
- currentCursor = `url(${cursorPenStarSvg}) 1 1, none`;
+ currentCursor = `url(${cursorPenStarSvg}) 0 0, none`;
}
}
}
-
el.style.cursor = currentCursor;
});
// Apply current pan state
- watch([cursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
+ watch([lastCursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
if (!dragState) return;
const [mode] = dragState;
if (mode !== 'view') return;
@@ -207,58 +233,109 @@ export function useInputMachine(
});
});
- // Update the UI canvas
+ // Update Selection UI
watch(
- [canvasRef, matrixRef, canvasPositionRef, selectedLayerRef],
- ([el, matrix, canvasPoint, selectedLayer]) => {
+ [selCanvasRef, stageResRef, dragStateRef, lastCursorPositionRef],
+ ([el, [sWidth, sHeight], dragState, p1]) => {
if (!el) return;
+ setCanvasDimensions(el, sWidth, sHeight);
+ if (!dragState) return;
- const [sWidth, sHeight] = unref(stageResRef);
- el.width = sWidth;
- el.height = sHeight;
const ctx = get2dContext(el);
ctx.clearRect(0, 0, sWidth, sHeight);
- // Draw precision cursor
- if (
+ const [dragMode] = dragState;
+ if (dragMode === 'sel-rect') {
+ const [, p0] = dragState;
+ ctx.save();
+ const selRect = rect(p0, p1);
+ pathPoly(ctx, selRect);
+ ctx.fillStyle = 'rgba(42 82 190 / 25%)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(42 82 190 / 100%)';
+ ctx.stroke();
+ ctx.restore();
+ }
+ },
+ );
+
+ watch([selCanvasRef, dragStateRef], ([el, dragState]) => {
+ if (!el || dragState) return;
+ setCanvasDimensions(el, 1, 1);
+ const ctx = get2dContext(el);
+ ctx.clearRect(0, 0, 1, 1);
+ });
+
+ // Update the cursor layer
+ watch(
+ [
+ cursorCanvasRef,
+ stageResRef,
+ matrixRef,
+ lastCursorPositionRef,
+ canvasPositionRef,
+ activeLayerRef,
+ ],
+ ([el, [sWidth, sHeight], matrix, screenPoint, canvasPoint]) => {
+ if (!el) return;
+ setCanvasDimensions(el, sWidth, sHeight);
+ const ctx = get2dContext(el);
+ ctx.clearRect(0, 0, sWidth, sHeight);
+
+ const simpleCusor = !(
toolbarStore.selectedTool === 'line' ||
toolbarStore.selectedTool === 'fill'
- ) {
- const [cWidth, cHeight] = unref(canvasResRef);
- const overCanvas = isInsideBounds([cWidth, cHeight], canvasPoint);
-
- if (overCanvas) {
- ctx.save();
- pixelBorder(ctx, applyToPoints(matrix, pixel(canvasPoint, 0.0125)));
- ctx.restore();
- }
- }
+ );
+
+ if (simpleCusor) return;
- if (selectedLayer) {
+ // Draw precision cursor
+ const [cWidth, cHeight] = unref(canvasResRef);
+ const overCanvas = isInsideBounds([cWidth, cHeight], canvasPoint);
+ if (overCanvas) {
ctx.save();
+ pixelBorder(ctx, applyToPoints(matrix, pixel(canvasPoint, -0.0125)));
+ cursorDot(ctx, screenPoint);
+ ctx.restore();
+ }
+ },
+ );
- const radius = unref(pointerRadiusRef);
- const found = nearestPointWithRange(
- selectedLayer.commands,
- canvasPoint,
- radius,
- );
+ // Update the UI canvas
+ watch(
+ [canvasRef, stageResRef, matrixRef, activeLayerRef, selectionStateRef],
+ ([el, [sWidth, sHeight], matrix, layer, selection]) => {
+ if (!el) return;
+ setCanvasDimensions(el, sWidth, sHeight);
+ const ctx = get2dContext(el);
+ ctx.clearRect(0, 0, sWidth, sHeight);
- selectedLayer.commands.forEach((cmd, idx) => {
+ if (layer) {
+ ctx.save();
+ layer.commands.forEach((cmd) => {
const [type] = cmd;
ctx.strokeStyle = 'white';
ctx.fillStyle = '#ddd';
- if (type === 'PLINE') {
- const highlight = found?.[0] == idx ? [found[1]] : [];
- plineSkeleton(ctx, matrix, cmd, highlight);
- }
-
- if (type === 'FILL') {
- const highlight = found?.[0] === idx;
- fillSkeleton(ctx, matrix, cmd, highlight);
- }
+ if (type === 'PLINE') plineSkeleton(ctx, matrix, cmd);
+ if (type === 'FILL') fillSkeleton(ctx, matrix, cmd);
});
+ ctx.restore();
}
+
+ ctx.save();
+ ctx.fillStyle = 'white';
+ selection.forEach(([lIdx, cIdx, vIdx]) => {
+ const layers = unref(picStore.layers);
+ const cmd = layers[lIdx]?.commands?.[cIdx];
+ if (!cmd) return;
+ const [type] = cmd;
+ if (type === 'PLINE' || type === 'FILL') {
+ const vertex = extractVertices(cmd)[vIdx];
+ if (!vertex) return;
+ pointSkeleton(ctx, matrix, vertex);
+ }
+ });
+ ctx.restore();
},
);
@@ -339,7 +416,7 @@ export function useInputMachine(
e.preventDefault();
},
- downSelect(e: PointerEvent) {
+ downPointSelect(e: PointerEvent) {
const { selectedTool } = toolbarStore;
if (!(selectedTool === 'select' && e.button === 0)) return;
@@ -364,8 +441,13 @@ export function useInputMachine(
e.preventDefault();
return;
}
+ },
- // start rectangle selector?
+ downPointStartRect(e: PointerEvent) {
+ const { selectedTool } = toolbarStore;
+ if (!(selectedTool === 'select' && e.button === 0)) return;
+ selectionStateRef.value = [];
+ dragStateRef.value = ['sel-rect', vec2(e.offsetX, e.offsetY)];
},
downFill(e: MouseEvent) {
@@ -472,9 +554,40 @@ export function useInputMachine(
},
move(e: PointerEvent) {
- cursorPositionRef.value = [e.offsetX, e.offsetY];
+ lastCursorPositionRef.value = [e.offsetX, e.offsetY];
+
+ const dragState = unref(dragStateRef);
+ if (dragState === null) return;
+
+ const [mode] = dragState;
+
+ // TODO rethink this
+ const layerIdx = unref(picStore.selection);
+ if (layerIdx === null) return;
+ const selectedLayer = unref(picStore.layers)[layerIdx];
+ if (!selectedLayer) return;
+ if (selectedLayer.type !== 'PLINE') return;
+
+ if (mode === 'sel-rect') {
+ const [, p0] = dragState;
+ const p1 = vec2(e.offsetX, e.offsetY);
+
+ const iMatrix = unref(iMatrixRef);
+ const bounds = applyToPoints(iMatrix, rect(p0, p1));
+
+ const selPoints: SelectionEntry[] = [];
+ selectedLayer.commands.forEach((cmd, cmdIdx) => {
+ const verts = extractVertices(cmd);
+ verts.forEach((v, vIdx) => {
+ if (!isInsidePolygon(bounds, v)) return;
+ selPoints.push([layerIdx, cmdIdx, vIdx]);
+ });
+ });
+
+ selectionStateRef.value = selPoints;
+ }
},
- up() {
+ up(_: PointerEvent) {
dragStateRef.value = null;
},
};
@@ -485,7 +598,8 @@ export function useInputMachine(
el.addEventListener('contextmenu', mouseHandlers.contextMenu);
el.addEventListener('wheel', mouseHandlers.wheel);
el.addEventListener('pointerdown', pointerHandlers.downPan);
- el.addEventListener('pointerdown', pointerHandlers.downSelect);
+ el.addEventListener('pointerdown', pointerHandlers.downPointSelect);
+ el.addEventListener('pointerdown', pointerHandlers.downPointStartRect);
el.addEventListener('pointerdown', pointerHandlers.downFill);
el.addEventListener('pointerdown', pointerHandlers.downLine);
el.addEventListener('pointermove', pointerHandlers.move);
@@ -499,7 +613,8 @@ export function useInputMachine(
el.removeEventListener('pointermove', pointerHandlers.move);
el.removeEventListener('pointerdown', pointerHandlers.downLine);
el.removeEventListener('pointerdown', pointerHandlers.downFill);
- el.removeEventListener('pointerdown', pointerHandlers.downSelect);
+ el.removeEventListener('pointerdown', pointerHandlers.downPointStartRect);
+ el.removeEventListener('pointerdown', pointerHandlers.downPointSelect);
el.removeEventListener('pointerdown', pointerHandlers.downPan);
el.removeEventListener('wheel', mouseHandlers.wheel);
el.removeEventListener('contextmenu', mouseHandlers.contextMenu);
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
index 3a83314..5de07bc 100644
--- a/apps/scishop/src/helpers/command-helpers.ts
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -12,6 +12,7 @@ import {
} from '@4bitlabs/vec2';
import { insert } from './array-helpers.ts';
import { exhaustive } from './exhaustive.ts';
+import { getSegments } from './polygons.ts';
export const extractVertices = (
cmd: PolylineCommand | FillCommand | BrushCommand,
@@ -89,11 +90,6 @@ export const moveLineVertex = (
pos: [number, number],
): PolylineCommand => [type, mode, codes, ...insert(verts, idx, pos, true)];
-function* getSegments(points: Vec2[]): Generator<[number, number, Vec2, Vec2]> {
- for (let i = 0; i < points.length - 1; i++)
- yield [i, i + 1, points[i], points[i + 1]];
-}
-
export const moveFillVertex = (
[type, mode, codes]: FillCommand,
pos: [number, number],
diff --git a/apps/scishop/src/helpers/polygons.ts b/apps/scishop/src/helpers/polygons.ts
index bea9c79..d2f9e9a 100644
--- a/apps/scishop/src/helpers/polygons.ts
+++ b/apps/scishop/src/helpers/polygons.ts
@@ -33,3 +33,40 @@ export const pathPoly = (
points.forEach(([x, y], i) => ctx[i === 0 ? 'moveTo' : 'lineTo'](x, y));
ctx.closePath();
};
+
+export const rect = ([x0, y0]: Vec2, [x1, y1]: Vec2): Vec2[] => [
+ [x0, y0],
+ [x1, y0],
+ [x1, y1],
+ [x0, y1],
+];
+
+export function* getSegments(
+ points: Vec2[],
+ loop = false,
+): Generator<[number, number, Vec2, Vec2]> {
+ const length = points.length;
+ for (let i = 0; i < points.length - (loop ? 0 : 1); i++)
+ yield [i, i + 1, points[i], points[(i + 1) % length]];
+}
+
+export const isInsidePolygon = (
+ verts: Vec2[],
+ [x, y]: Vec2,
+ loop = true,
+): boolean => {
+ let inside = false;
+
+ for (const [, , [x0, y0], [x1, y1]] of getSegments(verts, loop)) {
+ if (y <= Math.min(y0, y1)) continue;
+ if (y > Math.max(y0, y1)) continue;
+ if (x > Math.max(x0, x1)) continue;
+
+ const x_intersection = ((y - y0) * (x1 - x0)) / (y1 - y0) + x0;
+ if (!(x0 === x1 || x <= x_intersection)) continue;
+
+ inside = !inside;
+ }
+
+ return inside;
+};
diff --git a/apps/scishop/src/helpers/setCanvasDimensions.ts b/apps/scishop/src/helpers/setCanvasDimensions.ts
new file mode 100644
index 0000000..473f042
--- /dev/null
+++ b/apps/scishop/src/helpers/setCanvasDimensions.ts
@@ -0,0 +1,11 @@
+export function setCanvasDimensions(
+ target: { height: number; width: number },
+ width: number,
+ height: number,
+) {
+ const isChanged = target.width !== width || target.height !== height;
+ if (isChanged) {
+ target.width = width;
+ target.height = height;
+ }
+}
diff --git a/apps/scishop/src/render/cursor-dot.ts b/apps/scishop/src/render/cursor-dot.ts
index 97e8dbf..507c26d 100644
--- a/apps/scishop/src/render/cursor-dot.ts
+++ b/apps/scishop/src/render/cursor-dot.ts
@@ -7,14 +7,17 @@ export function cursorDot(
) {
ctx.save();
+ ctx.translate(x, y);
+
ctx.beginPath();
- ctx.arc(x, y, size * 1.5, 0, Math.PI * 2);
- ctx.fillStyle = 'black';
- ctx.fill();
+ ctx.arc(0, 0, size * 1.75, 0, Math.PI * 2);
+ ctx.arc(0, 0, size, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(0 0 0 / 20%)';
+ ctx.fill('evenodd');
ctx.beginPath();
- ctx.arc(x, y, size, 0, Math.PI * 2);
- ctx.fillStyle = 'white';
+ ctx.arc(0, 0, size, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(255 255 255 / 40%)';
ctx.fill();
ctx.restore();
diff --git a/apps/scishop/src/render/fill-skeleton.ts b/apps/scishop/src/render/fill-skeleton.ts
index 34f4d49..3c393e6 100644
--- a/apps/scishop/src/render/fill-skeleton.ts
+++ b/apps/scishop/src/render/fill-skeleton.ts
@@ -6,7 +6,7 @@ export function fillSkeleton(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
matrix: Matrix,
cmd: FillCommand,
- highlight: boolean,
+ highlight: boolean = false,
) {
const [, , , ...points] = cmd;
const all = applyToPoints(
diff --git a/apps/scishop/src/render/pline-skeleton.ts b/apps/scishop/src/render/pline-skeleton.ts
index a835968..50534d2 100644
--- a/apps/scishop/src/render/pline-skeleton.ts
+++ b/apps/scishop/src/render/pline-skeleton.ts
@@ -26,8 +26,8 @@ export function plineSkeleton(
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
- ctx.clearRect(-2, -2, 4, 4);
- ctx.roundRect(-2.5, -2.5, 5, 5, 0.5);
+ ctx.clearRect(-4, -4, 8, 8);
+ ctx.roundRect(-2.5, -2.5, 5, 5, 0);
ctx.stroke();
if (highlights.includes(idx)) ctx.fill();
ctx.restore();
diff --git a/apps/scishop/src/render/point-skeleton.ts b/apps/scishop/src/render/point-skeleton.ts
new file mode 100644
index 0000000..c85509e
--- /dev/null
+++ b/apps/scishop/src/render/point-skeleton.ts
@@ -0,0 +1,23 @@
+import { applyToPoint, Matrix } from 'transformation-matrix';
+
+import { Vec2 } from '@4bitlabs/vec2';
+
+export function pointSkeleton(
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ matrix: Matrix,
+ p: Vec2,
+ size = 8,
+ fill = true,
+) {
+ const [x, y] = applyToPoint(matrix, [p[0] + 0.5, p[1] + 0.5]);
+
+ ctx.save();
+ ctx.lineWidth = 1.5;
+ ctx.translate(x, y);
+ ctx.beginPath();
+ ctx.clearRect(-((size * 2) / 2), -((size * 2) / 2), size * 2, size * 2);
+ ctx.roundRect(-(size / 2), -(size / 2), size, size, 1);
+ ctx.stroke();
+ if (fill) ctx.fill();
+ ctx.restore();
+}
From b2524625ed3fe8c7a175eecb59f283701326e0c9 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Thu, 30 May 2024 09:06:20 -0600
Subject: [PATCH 18/28] WIP: updating sci0@3 and changes for the underlying
DrawCommand changes
---
apps/scishop/package.json | 2 +-
.../scishop/src/components/LayerNavigator.vue | 3 +-
.../command-items/BrushCommandItem.vue | 4 +--
.../command-items/FillCommandItem.vue | 3 +-
.../command-items/PolyLineCommandItem.vue | 3 +-
.../src/composables/useInputMachine.ts | 29 +++++++--------
apps/scishop/src/helpers/command-helpers.ts | 24 ++++++-------
apps/scishop/src/helpers/palette-helpers.ts | 4 +--
apps/scishop/src/render/fill-skeleton.ts | 2 +-
apps/scishop/src/render/pline-skeleton.ts | 2 +-
package-lock.json | 36 +++++++++----------
11 files changed, 56 insertions(+), 56 deletions(-)
diff --git a/apps/scishop/package.json b/apps/scishop/package.json
index e50068b..f1fb316 100644
--- a/apps/scishop/package.json
+++ b/apps/scishop/package.json
@@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@4bitlabs/sci0": "^2.7.0",
+ "@4bitlabs/sci0": "^3.0.0",
"@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
"@4bitlabs/image": "^3.3.9",
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index 7b72bec..c5dc54a 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -42,7 +42,8 @@ const handleClick = (e: MouseEvent, idx: number) => {
const ecmd = store.layers[idx];
const lastCmd = ecmd.commands.findLast(isHasDrawModes);
if (lastCmd) {
- const [, drawMode, drawCodes] = lastCmd;
+ const [, options] = lastCmd;
+ const [drawMode, drawCodes] = options;
drawState.value = [drawMode, ...drawCodes];
}
};
diff --git a/apps/scishop/src/components/command-items/BrushCommandItem.vue b/apps/scishop/src/components/command-items/BrushCommandItem.vue
index 9b2e24b..3381ae3 100644
--- a/apps/scishop/src/components/command-items/BrushCommandItem.vue
+++ b/apps/scishop/src/components/command-items/BrushCommandItem.vue
@@ -7,8 +7,8 @@ const { command, pals } = defineProps<{
pals: [number[], number[], number[], number[]];
}>();
-const [, drawMode, drawCode, patternCode] = command;
-const [size, isRect, isSpray] = patternCode;
+const [, options] = command;
+const [drawMode, drawCode, size, isRect, isSpray] = options;
diff --git a/apps/scishop/src/components/command-items/FillCommandItem.vue b/apps/scishop/src/components/command-items/FillCommandItem.vue
index a471313..1bf5d51 100644
--- a/apps/scishop/src/components/command-items/FillCommandItem.vue
+++ b/apps/scishop/src/components/command-items/FillCommandItem.vue
@@ -7,7 +7,8 @@ const { command, pals } = defineProps<{
pals: [number[], number[], number[], number[]];
}>();
-const [, drawMode, drawCode] = command;
+const [, options] = command;
+const [drawMode, drawCode] = options;
diff --git a/apps/scishop/src/components/command-items/PolyLineCommandItem.vue b/apps/scishop/src/components/command-items/PolyLineCommandItem.vue
index b920968..fb8818c 100644
--- a/apps/scishop/src/components/command-items/PolyLineCommandItem.vue
+++ b/apps/scishop/src/components/command-items/PolyLineCommandItem.vue
@@ -7,7 +7,8 @@ const { command, pals } = defineProps<{
pals: [number[], number[], number[], number[]];
}>();
-const [, drawMode, drawCode, ...coords] = command;
+const [, options, ...coords] = command;
+const [drawMode, drawCode] = options;
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index 2f733f2..b4addb2 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -343,10 +343,10 @@ export function useInputMachine(
const current = cmdStore.current;
if (current === null) return;
if (current.type === 'PLINE') {
- const [type, mode, code, ...coords] = current.commands[0];
+ const [type, options, ...coords] = current.commands[0];
cmdStore.begin({
...current,
- commands: [[type, mode, code, ...coords.slice(0, -1), pos]],
+ commands: [[type, options, ...coords.slice(0, -1), pos]],
});
}
});
@@ -359,13 +359,13 @@ export function useInputMachine(
const current = cmdStore.current;
if (current === null || current.type !== 'PLINE') return;
- const [type, mode, code, ...coords] = current.commands[0];
+ const [type, options, ...coords] = current.commands[0];
if (coords.length < 3) {
cmdStore.abort();
} else {
picStore.selection = cmdStore.commit({
...current,
- commands: [[type, mode, code, ...coords.slice(0, -1)]],
+ commands: [[type, options, ...coords.slice(0, -1)]],
});
}
return;
@@ -466,7 +466,7 @@ export function useInputMachine(
picStore.selection = cmdStore.commit({
id: Math.random().toString(36).substring(2),
type: 'FILL',
- commands: [['FILL', drawMode, drawCodes, pos]],
+ commands: [['FILL', [drawMode, drawCodes], pos]],
});
e.preventDefault();
@@ -488,11 +488,11 @@ export function useInputMachine(
// Append to current line
if (current?.type === 'PLINE') {
- const [type, mode, code, ...coords] = current.commands[0];
+ const [type, options, ...coords] = current.commands[0];
cmdStore.begin({
...current,
commands: [
- [type, mode, code, ...coords.slice(0, -1), [...pos], [...pos]],
+ [type, options, ...coords.slice(0, -1), [...pos], [...pos]],
],
});
e.preventDefault();
@@ -506,13 +506,13 @@ export function useInputMachine(
picStore.updateSelection((prev) => {
if (prev.type !== 'PLINE') return prev;
- const [type, mode, codes, ...prevVerts] = prev.commands[cmdIdx];
+ const [type, options, ...prevVerts] = prev.commands[cmdIdx];
const nextVerts = remove(prevVerts, pointIdx);
if (nextVerts.length <= 1) return null;
return {
...prev,
- commands: [[type, mode, codes, ...nextVerts]],
+ commands: [[type, options, ...nextVerts]],
};
});
e.preventDefault();
@@ -525,7 +525,7 @@ export function useInputMachine(
const [cmdIdx, , idx, vert] = nearestAddPoint;
picStore.updateSelection((prev) => {
if (prev.type !== 'PLINE') return prev;
- const [type, mode, codes, ...prevVerts] = prev.commands[cmdIdx];
+ const [type, options, ...prevVerts] = prev.commands[cmdIdx];
const nextVerts = insert(
prevVerts,
idx,
@@ -533,7 +533,7 @@ export function useInputMachine(
);
return {
...prev,
- commands: [[type, mode, codes, ...nextVerts]],
+ commands: [[type, options, ...nextVerts]],
};
});
e.preventDefault();
@@ -546,7 +546,7 @@ export function useInputMachine(
cmdStore.begin({
id: Math.random().toString(36).substring(2),
type: 'PLINE',
- commands: [['PLINE', drawMode, drawCodes, pos, pos]],
+ commands: [['PLINE', [drawMode, drawCodes], pos, pos]],
});
e.preventDefault();
return;
@@ -566,7 +566,8 @@ export function useInputMachine(
if (layerIdx === null) return;
const selectedLayer = unref(picStore.layers)[layerIdx];
if (!selectedLayer) return;
- if (selectedLayer.type !== 'PLINE') return;
+ if (selectedLayer.type !== 'PLINE' && selectedLayer.type !== 'FILL')
+ return;
if (mode === 'sel-rect') {
const [, p0] = dragState;
@@ -587,7 +588,7 @@ export function useInputMachine(
selectionStateRef.value = selPoints;
}
},
- up(_: PointerEvent) {
+ up() {
dragStateRef.value = null;
},
};
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
index 5de07bc..5ff672a 100644
--- a/apps/scishop/src/helpers/command-helpers.ts
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -1,5 +1,5 @@
import {
- BrushCommand,
+ DrawCommandStruct,
DrawCommand,
FillCommand,
PolylineCommand,
@@ -11,28 +11,24 @@ import {
Vec2,
} from '@4bitlabs/vec2';
import { insert } from './array-helpers.ts';
-import { exhaustive } from './exhaustive.ts';
import { getSegments } from './polygons.ts';
export const extractVertices = (
- cmd: PolylineCommand | FillCommand | BrushCommand,
+ cmd: DrawCommandStruct,
): Vec2[] => {
const [type] = cmd;
switch (type) {
case 'PLINE': {
- const [, , , ...vertices] = cmd;
+ const [, , ...vertices] = cmd;
return vertices;
}
+ case 'BRUSH':
case 'FILL': {
- const [, , , vertex] = cmd;
- return [vertex];
- }
- case 'BRUSH': {
- const [, , , , , vertex] = cmd;
+ const [, , vertex] = cmd;
return [vertex];
}
default:
- exhaustive(`unexpected type`, type);
+ return [];
}
};
@@ -85,15 +81,15 @@ export function nearestPointWithRange(
}
export const moveLineVertex = (
- [type, mode, codes, ...verts]: PolylineCommand,
+ [type, options, ...verts]: PolylineCommand,
idx: number,
pos: [number, number],
-): PolylineCommand => [type, mode, codes, ...insert(verts, idx, pos, true)];
+): PolylineCommand => [type, options, ...insert(verts, idx, pos, true)];
export const moveFillVertex = (
- [type, mode, codes]: FillCommand,
+ [type, options]: FillCommand,
pos: [number, number],
-): FillCommand => [type, mode, codes, pos];
+): FillCommand => [type, options, pos];
export type PointAlongPathResult = [
cmdIdx: number,
diff --git a/apps/scishop/src/helpers/palette-helpers.ts b/apps/scishop/src/helpers/palette-helpers.ts
index 87782a6..f91aa79 100644
--- a/apps/scishop/src/helpers/palette-helpers.ts
+++ b/apps/scishop/src/helpers/palette-helpers.ts
@@ -21,13 +21,13 @@ export const mapToPals = (commands: EditorCommand[]) => {
const { type } = editorCmd;
switch (type) {
case 'SET_PALETTE': {
- const [, palIdx, colors] = editorCmd.commands[0];
+ const [, [palIdx], ...colors] = editorCmd.commands[0];
const next: PaletteSet = [...prevSet];
next[palIdx] = [...colors];
return [...stack, next];
}
case 'UPDATE_PALETTE': {
- const [, entries] = editorCmd.commands[0];
+ const [, , ...entries] = editorCmd.commands[0];
const next = entries.reduce((pals, [palIdx, clrIdx, color]) => {
const nextSet: PaletteSet = [...prevSet];
const nextPal = [...nextSet[palIdx]];
diff --git a/apps/scishop/src/render/fill-skeleton.ts b/apps/scishop/src/render/fill-skeleton.ts
index 3c393e6..1a3d07e 100644
--- a/apps/scishop/src/render/fill-skeleton.ts
+++ b/apps/scishop/src/render/fill-skeleton.ts
@@ -8,7 +8,7 @@ export function fillSkeleton(
cmd: FillCommand,
highlight: boolean = false,
) {
- const [, , , ...points] = cmd;
+ const [, , ...points] = cmd;
const all = applyToPoints(
matrix,
points.map(([x, y]) => [x + 0.5, y + 0.5]),
diff --git a/apps/scishop/src/render/pline-skeleton.ts b/apps/scishop/src/render/pline-skeleton.ts
index 50534d2..0b3caf5 100644
--- a/apps/scishop/src/render/pline-skeleton.ts
+++ b/apps/scishop/src/render/pline-skeleton.ts
@@ -10,7 +10,7 @@ export function plineSkeleton(
) {
ctx.save();
- const [, , , ...cPoints] = cmd;
+ const [, , ...cPoints] = cmd;
const points = applyToPoints(
matrix,
cPoints.map(([x, y]) => [x + 0.5, y + 0.5]),
diff --git a/package-lock.json b/package-lock.json
index 4448119..8c874e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -74,7 +74,7 @@
"@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
"@4bitlabs/image": "^3.3.9",
- "@4bitlabs/sci0": "^2.7.0",
+ "@4bitlabs/sci0": "^3.0.0",
"@4bitlabs/vec2": "^1.0.1",
"fast-deep-equal": "^3.1.3",
"transformation-matrix": "^2.16.1",
@@ -88,15 +88,15 @@
}
},
"apps/scishop/node_modules/@4bitlabs/sci0": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@4bitlabs/sci0/-/sci0-2.7.0.tgz",
- "integrity": "sha512-5V8IatsqBitsaauwJZziR5vUWGPP8kaylsiFk4q/Oqx2xUMv4Cy8CnDLSJmdPYTqE0d4p3xkISLK6oMAKRVemg==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@4bitlabs/sci0/-/sci0-3.1.3.tgz",
+ "integrity": "sha512-d8pnvhMSig3L4MuRRQQlSsKeaUWWYKE2Okp7burwLG86jKLprr0ZkvZQegPIb5sMuytjXdFXNn8285LOtKanYw==",
"dependencies": {
- "@4bitlabs/codecs": "^2.0.1",
- "@4bitlabs/image": "^3.3.9",
- "@4bitlabs/numeric-deque": "^1.1.5",
- "@4bitlabs/readers": "^2.0.5",
- "@4bitlabs/vec2": "^1.0.0"
+ "@4bitlabs/codecs": "^2.0.2",
+ "@4bitlabs/image": "^3.3.11",
+ "@4bitlabs/numeric-deque": "^1.1.6",
+ "@4bitlabs/readers": "^2.0.6",
+ "@4bitlabs/vec2": "^1.2.0"
}
},
"libs/blur-filters": {
@@ -23067,7 +23067,7 @@
"@4bitlabs/color": "^2.1.4",
"@4bitlabs/color-space": "^1.2.2",
"@4bitlabs/image": "^3.3.9",
- "@4bitlabs/sci0": "^2.7.0",
+ "@4bitlabs/sci0": "^3.0.0",
"@4bitlabs/vec2": "^1.0.1",
"@vitejs/plugin-vue": "^5.0.4",
"fast-deep-equal": "^3.1.3",
@@ -23079,15 +23079,15 @@
},
"dependencies": {
"@4bitlabs/sci0": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@4bitlabs/sci0/-/sci0-2.7.0.tgz",
- "integrity": "sha512-5V8IatsqBitsaauwJZziR5vUWGPP8kaylsiFk4q/Oqx2xUMv4Cy8CnDLSJmdPYTqE0d4p3xkISLK6oMAKRVemg==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@4bitlabs/sci0/-/sci0-3.1.3.tgz",
+ "integrity": "sha512-d8pnvhMSig3L4MuRRQQlSsKeaUWWYKE2Okp7burwLG86jKLprr0ZkvZQegPIb5sMuytjXdFXNn8285LOtKanYw==",
"requires": {
- "@4bitlabs/codecs": "^2.0.1",
- "@4bitlabs/image": "^3.3.9",
- "@4bitlabs/numeric-deque": "^1.1.5",
- "@4bitlabs/readers": "^2.0.5",
- "@4bitlabs/vec2": "^1.0.0"
+ "@4bitlabs/codecs": "^2.0.2",
+ "@4bitlabs/image": "^3.3.11",
+ "@4bitlabs/numeric-deque": "^1.1.6",
+ "@4bitlabs/readers": "^2.0.6",
+ "@4bitlabs/vec2": "^1.2.0"
}
}
}
From 8a11419cdde52cc9cd99e350ffd82c07a8cfa104 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Thu, 30 May 2024 11:54:23 -0600
Subject: [PATCH 19/28] WIP: simplify drag-state shape
---
.../src/composables/useInputMachine.ts | 87 +++++++++----------
1 file changed, 43 insertions(+), 44 deletions(-)
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index b4addb2..bf9d8ac 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -70,6 +70,7 @@ const clampZoom = (current: number, next: number, min: number, max: number) => {
type SelectionEntry = [layerIdx: number, cmdIdx: number, vertexIdx: number];
+type EmptyDragState = ['none'];
type ViewDragState = ['view', iMatrix: Matrix, iPosition: Vec2];
type PointDragState = [
'point',
@@ -78,7 +79,11 @@ type PointDragState = [
];
type SelectionDragState = ['sel-rect', start: Vec2];
-type DragStates = ViewDragState | PointDragState | SelectionDragState;
+type DragStates =
+ | EmptyDragState
+ | ViewDragState
+ | PointDragState
+ | SelectionDragState;
const selectedLayerRef = computed(() => {
const selIdx = unref(picStore.selection);
@@ -102,7 +107,7 @@ export function useInputMachine(
canvasResRef: Ref<[number, number]>,
) {
const iMatrixRef = computed(() => inverse(unref(matrixRef)));
- const dragStateRef = shallowRef(null);
+ const dragStateRef = shallowRef(['none']);
const selectionStateRef = shallowRef([]);
const lastCursorPositionRef = useRafRef<[number, number]>([0, 0]);
@@ -155,11 +160,11 @@ export function useInputMachine(
const el = unref(canvasRef);
if (!el) return;
- const dragState = unref(dragStateRef);
+ const [dragMode] = unref(dragStateRef);
const { selectedTool } = toolbarStore;
let currentCursor = 'auto';
- if (dragState !== null && dragState[0] === 'view') {
+ if (dragMode === 'view') {
currentCursor = 'grabbing';
} else if (selectedTool === 'pan') {
currentCursor = 'grab';
@@ -183,8 +188,7 @@ export function useInputMachine(
});
// Apply current pan state
- watch([lastCursorPositionRef, dragStateRef], ([[cX, cY], dragState]) => {
- if (!dragState) return;
+ watch([dragStateRef, lastCursorPositionRef], ([dragState, [cX, cY]]) => {
const [mode] = dragState;
if (mode !== 'view') return;
@@ -239,28 +243,28 @@ export function useInputMachine(
([el, [sWidth, sHeight], dragState, p1]) => {
if (!el) return;
setCanvasDimensions(el, sWidth, sHeight);
- if (!dragState) return;
+
+ const [dragMode] = dragState;
+ if (dragMode !== 'sel-rect') return;
const ctx = get2dContext(el);
ctx.clearRect(0, 0, sWidth, sHeight);
- const [dragMode] = dragState;
- if (dragMode === 'sel-rect') {
- const [, p0] = dragState;
- ctx.save();
- const selRect = rect(p0, p1);
- pathPoly(ctx, selRect);
- ctx.fillStyle = 'rgba(42 82 190 / 25%)';
- ctx.fill();
- ctx.strokeStyle = 'rgba(42 82 190 / 100%)';
- ctx.stroke();
- ctx.restore();
- }
+ const [, p0] = dragState;
+ ctx.save();
+ const selRect = rect(p0, p1);
+ pathPoly(ctx, selRect);
+ ctx.fillStyle = 'rgba(42 82 190 / 25%)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(42 82 190 / 100%)';
+ ctx.stroke();
+ ctx.restore();
},
);
- watch([selCanvasRef, dragStateRef], ([el, dragState]) => {
- if (!el || dragState) return;
+ watch([selCanvasRef, dragStateRef], ([el, [dragMode]]) => {
+ if (!el) return;
+ if (dragMode !== 'none') return;
setCanvasDimensions(el, 1, 1);
const ctx = get2dContext(el);
ctx.clearRect(0, 0, 1, 1);
@@ -339,6 +343,7 @@ export function useInputMachine(
},
);
+ // update current
watch([canvasPixelRef], ([pos]) => {
const current = cmdStore.current;
if (current === null) return;
@@ -400,11 +405,10 @@ export function useInputMachine(
const pointerHandlers = {
downPan(e: PointerEvent) {
const { selectedTool } = toolbarStore;
- const isPanning =
+ const startPan =
(selectedTool === 'pan' && e.button === 0) || e.button === 1;
- if (!isPanning) return;
- if (dragStateRef.value !== null) return; // already panning
+ if (!startPan) return;
dragStateRef.value = [
'view',
@@ -557,10 +561,10 @@ export function useInputMachine(
lastCursorPositionRef.value = [e.offsetX, e.offsetY];
const dragState = unref(dragStateRef);
- if (dragState === null) return;
-
const [mode] = dragState;
+ if (mode !== 'sel-rect') return;
+
// TODO rethink this
const layerIdx = unref(picStore.selection);
if (layerIdx === null) return;
@@ -569,27 +573,22 @@ export function useInputMachine(
if (selectedLayer.type !== 'PLINE' && selectedLayer.type !== 'FILL')
return;
- if (mode === 'sel-rect') {
- const [, p0] = dragState;
- const p1 = vec2(e.offsetX, e.offsetY);
-
- const iMatrix = unref(iMatrixRef);
- const bounds = applyToPoints(iMatrix, rect(p0, p1));
-
- const selPoints: SelectionEntry[] = [];
- selectedLayer.commands.forEach((cmd, cmdIdx) => {
- const verts = extractVertices(cmd);
- verts.forEach((v, vIdx) => {
- if (!isInsidePolygon(bounds, v)) return;
- selPoints.push([layerIdx, cmdIdx, vIdx]);
- });
+ const [, p0] = dragState;
+ const p1 = vec2(e.offsetX, e.offsetY);
+ const iMatrix = unref(iMatrixRef);
+ const bounds = applyToPoints(iMatrix, rect(p0, p1));
+ const selPoints: SelectionEntry[] = [];
+ selectedLayer.commands.forEach((cmd, cmdIdx) => {
+ const verts = extractVertices(cmd);
+ verts.forEach((v, vIdx) => {
+ if (!isInsidePolygon(bounds, v)) return;
+ selPoints.push([layerIdx, cmdIdx, vIdx]);
});
-
- selectionStateRef.value = selPoints;
- }
+ });
+ selectionStateRef.value = selPoints;
},
up() {
- dragStateRef.value = null;
+ dragStateRef.value = ['none'];
},
};
From 0db24a25d042cb4df7acba36b385e62da4940429 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Thu, 30 May 2024 16:09:59 -0600
Subject: [PATCH 20/28] WIP: implementing mutli-select vertex move
---
.../src/composables/useInputMachine.ts | 83 ++++++++++++++-----
apps/scishop/src/helpers/command-helpers.ts | 9 ++
2 files changed, 71 insertions(+), 21 deletions(-)
diff --git a/apps/scishop/src/composables/useInputMachine.ts b/apps/scishop/src/composables/useInputMachine.ts
index bf9d8ac..981904c 100644
--- a/apps/scishop/src/composables/useInputMachine.ts
+++ b/apps/scishop/src/composables/useInputMachine.ts
@@ -43,6 +43,7 @@ import { pixelBorder } from '../render/pixel-border.ts';
import { fillSkeleton } from '../render/fill-skeleton.ts';
import { plineSkeleton } from '../render/pline-skeleton.ts';
import {
+ anyPointCloseTo,
extractVertices,
FindResult,
moveFillVertex,
@@ -72,11 +73,8 @@ type SelectionEntry = [layerIdx: number, cmdIdx: number, vertexIdx: number];
type EmptyDragState = ['none'];
type ViewDragState = ['view', iMatrix: Matrix, iPosition: Vec2];
-type PointDragState = [
- 'point',
- iPosition: Vec2,
- ...[...SelectionEntry, initial: Vec2][],
-];
+type PointDragEntry = [...SelectionEntry, initial: Vec2];
+type PointDragState = ['point', iPosition: Vec2, ...PointDragEntry[]];
type SelectionDragState = ['sel-rect', start: Vec2];
type DragStates =
@@ -207,22 +205,18 @@ export function useInputMachine(
const [, initialPosition, ...pairs] = dragState;
const delta = sub(currentPosition, initialPosition);
- const layers = unref(picStore.layers);
- pairs.forEach(([lIdx, cIdx, vIdx, iVec]) => {
+ layersRef.value = pairs.reduce((prevLayers, [lIdx, cIdx, vIdx, iVec]) => {
const nextVec = round(add(iVec, delta));
+ const layer = prevLayers[lIdx];
- const layer = layers[lIdx];
- if (!layer) return;
-
- switch (layer.type) {
+ switch (layer?.type) {
case 'PLINE': {
const cmd = layer.commands[cIdx];
const next: BasicEditorCommand = {
...layer,
commands: [moveLineVertex(cmd, vIdx, nextVec)],
};
- layersRef.value = insert(picStore.layers, lIdx, next, true);
- break;
+ return insert(prevLayers, lIdx, next, true);
}
case 'FILL': {
const cmd = layer.commands[cIdx];
@@ -230,11 +224,11 @@ export function useInputMachine(
...layer,
commands: [moveFillVertex(cmd, nextVec)],
};
- layersRef.value = insert(picStore.layers, lIdx, next, true);
- break;
+ return insert(prevLayers, lIdx, next, true);
}
}
- });
+ return prevLayers;
+ }, unref(picStore.layers));
});
// Update Selection UI
@@ -420,16 +414,58 @@ export function useInputMachine(
e.preventDefault();
},
+ downSelectionMoveStart(e: PointerEvent) {
+ const { selectedTool } = toolbarStore;
+ if (!(selectedTool === 'select' && e.button === 0)) return;
+
+ const layers = unref(picStore.layers);
+ const cPos = unref(canvasPositionRef);
+
+ const selState = unref(selectionStateRef);
+ const selectedVertices = selState.flatMap(([lIdx, cIdx, vIdx]) => {
+ const cmd = layers[lIdx]?.commands[cIdx];
+ if (!cmd) return [];
+ if (cmd[0] !== 'PLINE' && cmd[0] !== 'FILL') return [];
+ const vertex = extractVertices(cmd)[vIdx];
+ if (!vertex) return [];
+ const [x, y] = vertex;
+ return [vec2(x + 0.5, y + 0.5)];
+ });
+
+ const found = anyPointCloseTo(
+ selectedVertices,
+ cPos,
+ Math.max(0.5, 7 / viewStore.zoom),
+ );
+
+ if (!found) return;
+
+ const dragEntries = selState.map(([lIdx, cIdx, pIdx]) => [
+ lIdx,
+ cIdx,
+ pIdx,
+ mustGetVertexFrom(layers[lIdx].commands, cIdx, pIdx),
+ ]);
+
+ dragStateRef.value = ['point', cPos, ...dragEntries];
+
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ },
+
downPointSelect(e: PointerEvent) {
const { selectedTool } = toolbarStore;
if (!(selectedTool === 'select' && e.button === 0)) return;
- const lIdx = unref(picStore.selection);
- if (lIdx === null) return;
- const layer = unref(picStore.layers)[lIdx];
+ const layers = unref(picStore.layers);
+ const cPos = unref(canvasPositionRef);
+
+ const iIdx = unref(picStore.selection);
+ if (iIdx === null) return;
+
+ const layer = layers[iIdx];
if (!layer) return;
- const cPos = unref(canvasPositionRef);
const found = nearestPointWithRange(
layer.commands,
cPos,
@@ -439,7 +475,7 @@ export function useInputMachine(
if (found) {
const [cIdx, pIdx] = found;
const p = mustGetVertexFrom(layer.commands, cIdx, pIdx);
- dragStateRef.value = ['point', cPos, [lIdx, cIdx, pIdx, p]];
+ dragStateRef.value = ['point', cPos, [iIdx, cIdx, pIdx, p]];
e.stopImmediatePropagation();
e.preventDefault();
@@ -598,6 +634,7 @@ export function useInputMachine(
el.addEventListener('contextmenu', mouseHandlers.contextMenu);
el.addEventListener('wheel', mouseHandlers.wheel);
el.addEventListener('pointerdown', pointerHandlers.downPan);
+ el.addEventListener('pointerdown', pointerHandlers.downSelectionMoveStart);
el.addEventListener('pointerdown', pointerHandlers.downPointSelect);
el.addEventListener('pointerdown', pointerHandlers.downPointStartRect);
el.addEventListener('pointerdown', pointerHandlers.downFill);
@@ -615,6 +652,10 @@ export function useInputMachine(
el.removeEventListener('pointerdown', pointerHandlers.downFill);
el.removeEventListener('pointerdown', pointerHandlers.downPointStartRect);
el.removeEventListener('pointerdown', pointerHandlers.downPointSelect);
+ el.removeEventListener(
+ 'pointerdown',
+ pointerHandlers.downSelectionMoveStart,
+ );
el.removeEventListener('pointerdown', pointerHandlers.downPan);
el.removeEventListener('wheel', mouseHandlers.wheel);
el.removeEventListener('contextmenu', mouseHandlers.contextMenu);
diff --git a/apps/scishop/src/helpers/command-helpers.ts b/apps/scishop/src/helpers/command-helpers.ts
index 5ff672a..92a0ad5 100644
--- a/apps/scishop/src/helpers/command-helpers.ts
+++ b/apps/scishop/src/helpers/command-helpers.ts
@@ -44,6 +44,15 @@ export const findClosestPointTo = (
return thisD2 < prevD2 ? [thisD2, pIdx] : state;
}, initial);
+export const anyPointCloseTo = (
+ vertices: Readonly[],
+ position: Readonly,
+ proximity: number,
+): boolean => {
+ const [result] = findClosestPointTo(vertices, position);
+ return Math.sqrt(result) <= proximity;
+};
+
export type FindState = [dSqrd: number, commandIdx: number, pointIdx: number];
export const findClosestPointIn = (
commands: DrawCommand[],
From dcc9f13f81df909ac7f669f18e0084c1f3b88323 Mon Sep 17 00:00:00 2001
From: J Holmes <32bitkid@gmail.com>
Date: Thu, 30 May 2024 18:02:58 -0600
Subject: [PATCH 21/28] WIP: simplify stores
---
.../scishop/src/components/LayerNavigator.vue | 18 ++---
.../PalettePicker/PalettePicker.vue | 12 +--
apps/scishop/src/components/Stage.vue | 28 ++++---
apps/scishop/src/components/StatusBar.vue | 6 +-
apps/scishop/src/components/Toolbar.vue | 64 ++++-----------
.../src/composables/useCanvasRenderer.ts | 11 +--
.../src/composables/useInputMachine.ts | 81 ++++++++++---------
apps/scishop/src/data/paletteStore.ts | 9 ++-
apps/scishop/src/data/picStore.ts | 70 ++++------------
apps/scishop/src/data/stageStore.ts | 18 +----
apps/scishop/src/data/toolbarStore.ts | 13 +--
apps/scishop/src/data/updateSelection.ts | 26 ++++++
apps/scishop/src/data/viewStore.ts | 23 +-----
apps/scishop/src/helpers/clamp.ts | 2 +
apps/scishop/src/helpers/smoothstep.ts | 3 +-
15 files changed, 155 insertions(+), 229 deletions(-)
create mode 100644 apps/scishop/src/data/updateSelection.ts
create mode 100644 apps/scishop/src/helpers/clamp.ts
diff --git a/apps/scishop/src/components/LayerNavigator.vue b/apps/scishop/src/components/LayerNavigator.vue
index c5dc54a..4017bd1 100644
--- a/apps/scishop/src/components/LayerNavigator.vue
+++ b/apps/scishop/src/components/LayerNavigator.vue
@@ -1,7 +1,7 @@
diff --git a/apps/scishop/src/components/StatusBar.vue b/apps/scishop/src/components/StatusBar.vue
index 39ff1a0..43b3447 100644
--- a/apps/scishop/src/components/StatusBar.vue
+++ b/apps/scishop/src/components/StatusBar.vue
@@ -1,11 +1,11 @@
-
{{ viewStore.zoom.toFixed(1) }}×
+
{{ zoomRef.toFixed(1) }}×
- {{ (viewStore.rotate * (180 / Math.PI)).toFixed(0) }}°
+ {{ (rotateRef * (180 / Math.PI)).toFixed(0) }}°
diff --git a/apps/scishop/src/components/Toolbar.vue b/apps/scishop/src/components/Toolbar.vue
index 83462f7..6b7591d 100644
--- a/apps/scishop/src/components/Toolbar.vue
+++ b/apps/scishop/src/components/Toolbar.vue
@@ -1,96 +1,66 @@
- -
-