Skip to content

Commit

Permalink
Merge pull request #7 from RealA10N/dragables
Browse files Browse the repository at this point in the history
Added the Grabzone & Grabable components
  • Loading branch information
RealA10N committed Dec 22, 2023
2 parents 082fb2e + 932e140 commit 6fcefc7
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 38 deletions.
39 changes: 25 additions & 14 deletions src/lib/arrays/Array.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { blur } from 'svelte/transition';
<script lang="ts" context="module">
import type { BoxState as GenericBoxState, Stringable } from '$lib/interfaces/strings';
</script>

<script lang="ts" generics="Text extends Stringable">
import StringBox from '$lib/strings/StringBox.svelte';
import type { BoxState } from '$lib/interfaces/strings';
export let array: BoxState[];
import Grabzone from '$lib/grabs/Grabzone.svelte';
type BoxState = GenericBoxState<Text>;
export let items: BoxState[];
export let onGrab: (item: BoxState) => any = () => {};
export let onUpdate: (item: BoxState) => any = () => {};
export let onRelease: (item: BoxState) => any = () => {};
</script>

{#each array as state (state.text)}
<div
class="inline-block"
animate:flip={{ duration: 400 }}
in:blur={{ duration: 400 }}
out:blur={{ duration: 150 }}
>
<StringBox {state} />
<Grabzone
{items}
let:item
let:dummy
flipOptions={{ duration: 400 }}
{onGrab}
{onRelease}
{onUpdate}
>
<div class="inline-block" class:opacity-50={dummy}>
<StringBox state={item} />
</div>
{/each}
</Grabzone>
44 changes: 27 additions & 17 deletions src/lib/arrays/InsertionSort.svelte
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
<script lang="ts">
import type { BoxState } from '$lib/interfaces/strings';
import type { BoxState as GenericBoxState } from '$lib/interfaces/strings';
import Array from '$lib/arrays/Array.svelte';
import { shuffle } from '$lib/arrays/permutation';
import Figure from '$lib/Figure.svelte';
import ConfettiWrapper from '$lib/effects/ConfettiWrapper.svelte';
import AnimationButton from '$lib/AnimationButton.svelte';
import { Color } from '../graphs/graphs';
type BoxState = GenericBoxState<number>;
interface State {
processing: number | null;
goods: number;
array: number[];
}
const init = (array: number[]) =>
({
processing: null,
goods: 0,
array: [...array]
} as State);
const init = (array: number[]) => ({
processing: null,
goods: 0,
array: [...array]
});
export let array: number[];
$: state = init(array);
$: n = array.length;
$: uiArray = buildUiArray(state);
const buildUiArray = (state: State): BoxState[] =>
state.array.map(
(val, idx) =>
({
text: val,
highlight: idx === state.processing,
color: isCorrect(idx) ? 'green' : undefined
} as BoxState)
);
state.array.map((val, idx) => ({
id: val.toString(),
text: val,
highlight: idx === state.processing,
color: isCorrect(idx) ? Color.Green : undefined
}));
$: uiArray = buildUiArray(state);
const next = () => {
if (state.processing === null) {
Expand All @@ -58,11 +59,20 @@
const isCorrect = (idx: number): boolean =>
idx !== state.processing && idx < state.goods + (state.processing !== null ? 1 : 0);
const reset = () => (state = init(state.array));
const updateFromUser = () => (state.array = uiArray.map((state) => state.text));
</script>

<ConfettiWrapper bind:trigger={confetti}>
<Figure>
<Array array={uiArray} slot="content" />
<Array
items={uiArray}
slot="content"
onGrab={() => (stop(), reset())}
onUpdate={updateFromUser}
/>
<svelte:fragment slot="buttons">
<AnimationButton {next} bind:stop interval={800} />
<button on:click={() => (stop(), next())}>Next</button>
Expand Down
22 changes: 19 additions & 3 deletions src/lib/arrays/PermutationShuffle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
import Figure from '$lib/Figure.svelte';
import Array from '$lib/arrays/Array.svelte';
import AnimationButton from '$src/lib/AnimationButton.svelte';
import { factorial, idxToPerm, permIdx, shuffle } from '$lib/arrays/permutation';
import {
factorial,
idxToPerm,
permIdx,
shuffle,
type Permutation
} from '$lib/arrays/permutation';
type State = BoxState<bigint>;
export let n: bigint = 8n;
export let playOnMount = true;
Expand All @@ -17,7 +25,7 @@
$: idx = permIdx(permutation, []);
$: fact = factorial(n);
$: permutation, validateUserInput();
$: array = permutation.map((val) => ({ text: val } as BoxState));
$: items = permutationToItems(permutation);
const shfl = () => (permutation = shuffle(permutation));
const next = () => (permutation = idxToPerm((idx + 1n) % fact, n));
Expand All @@ -39,10 +47,18 @@
return invalidateUserIdx();
}
};
const permutationToItems = (permutation: Permutation): State[] =>
permutation.map((val) => ({ text: val + 1n, id: val.toString() } as State));
const itemsToPermutation = (items: State[]): Permutation =>
items.map((item) => (item.text - 1n) as bigint);
const updateFromUser = () => (permutation = itemsToPermutation(items));
</script>

<Figure>
<Array slot="content" {array} />
<Array slot="content" {items} onGrab={stop} onUpdate={updateFromUser} />

<svelte:fragment slot="buttons">
<AnimationButton next={animate} bind:stop {playOnMount} />
Expand Down
46 changes: 46 additions & 0 deletions src/lib/grabs/Grabable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { P } from './utils';
let grabbingPointer: number | undefined;
$: grabbed = grabbingPointer !== undefined;
let dummyPosition: P, grabOffset: P;
const grab = (e: PointerEvent) => {
grabbingPointer = e.pointerId;
grabOffset = { x: e.offsetX, y: e.offsetY };
updateDummy(e);
};
const release = (e: PointerEvent) => {
if (e.pointerId === grabbingPointer) grabbingPointer = undefined;
};
const move = (e: PointerEvent) => {
if (e.pointerId === grabbingPointer) updateDummy(e);
};
const updateDummy = (e: PointerEvent) =>
(dummyPosition = { x: e.clientX - grabOffset.x, y: e.clientY - grabOffset.y });
onMount(() => {
window.addEventListener('pointerup', release);
window.addEventListener('pointermove', move);
return () => {
window.removeEventListener('pointerup', release);
window.removeEventListener('pointermove', move);
};
});
</script>

<span class="inline-block select-none cursor-grab touch-none" on:pointerdown={grab}>
<slot dummy={grabbed} />
</span>

{#if grabbed}
<span
class="inline-block select-none fixed cursor-grabbing"
style="top: {dummyPosition.y}px; left: {dummyPosition.x}px"
>
<slot name="floating" dummy={false} />
</span>
{/if}
98 changes: 98 additions & 0 deletions src/lib/grabs/Grabzone.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script context="module" lang="ts">
import type { Id, Identifiable } from './utils';
</script>

<script lang="ts" generics="Item extends Identifiable">
import { onMount } from 'svelte';
import { flip, type FlipParams } from 'svelte/animate';
import { findClosestIdx, elementPos, type P, moveByIdx, findIdx } from './utils';
export let flipOptions: FlipParams = {};
export let items: Item[];
let grabbedItem: Item | undefined;
let elements = [] as HTMLElement[];
let elementsPos = [] as P[];
$: isGrabbed = grabbedItem !== undefined;
let grabbingPointerId: number = -1;
let grabPos: P, grabOffset: P;
// callbacks for user of compoennt
export let onGrab: (item: Item) => any = () => {};
export let onUpdate: (item: Item) => any = () => {};
export let onRelease: (item: Item) => any = () => {};
const grab = (e: PointerEvent, item: Item) => {
if (isGrabbed) return;
grabbedItem = item;
grabbingPointerId = e.pointerId;
grabOffset = { x: e.offsetX, y: e.offsetY };
saveElementsPositions();
updateFloatingPos(e);
updateItemsPositions();
onGrab(item);
};
const drag = (e: PointerEvent) => {
if (!isEventRelevent(e)) return;
updateFloatingPos(e);
updateItemsPositions();
};
const release = (e: PointerEvent) => {
if (!isEventRelevent(e)) return;
onRelease(grabbedItem!);
grabbedItem = undefined;
};
const isEventRelevent = (e: PointerEvent) => isGrabbed && e.pointerId === grabbingPointerId;
const updateFloatingPos = (e: PointerEvent) =>
(grabPos = { x: e.clientX - grabOffset.x, y: e.clientY - grabOffset.y });
const updateItemsPositions = () => {
const closestIdx = findClosestIdx(grabPos, elementsPos)!;
const grabbedIdx = findIdx(items, grabbedItem!.id);
if (closestIdx === grabbedIdx) return;
moveByIdx(items, grabbedIdx, closestIdx);
items = items;
onUpdate(grabbedItem!);
};
const saveElementsPositions = () => {
for (const [idx, elem] of elements.entries()) elementsPos[idx] = elementPos(elem);
};
onMount(() => {
window.addEventListener('pointerup', release);
window.addEventListener('pointermove', drag);
return () => {
window.removeEventListener('pointerup', release);
window.removeEventListener('pointermove', drag);
};
});
</script>

{#each items as item, idx (item.id)}
<span animate:flip={flipOptions} on:pointerdown={(e) => grab(e, item)} bind:this={elements[idx]}>
<slot {item} dummy={item === grabbedItem} />
</span>
{/each}

{#if grabbedItem !== undefined}
{#key items}
<span class="grabbed fixed" style="left: {grabPos.x}px; top: {grabPos.y}px">
<slot item={grabbedItem} dummy={false} />
</span>
{/key}
{/if}

<style lang="postcss">
span:not(.grabbed) {
@apply inline-block select-none touch-none cursor-grab;
}
span.grabbed {
@apply cursor-grabbing;
}
</style>
35 changes: 35 additions & 0 deletions src/lib/grabs/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type Id = string;
export interface Identifiable {
id: Id;
}

export interface P {
x: number;
y: number;
}

const abs = Math.abs;
const dx = (a: P, b: P) => abs(a.x - b.x);
const dy = (a: P, b: P) => abs(a.y - b.y);

export const dist = (a: P, b: P): number => dx(a, b) + dy(a, b);

export const elementPos = (e: HTMLElement): P => e.getBoundingClientRect() as P;

export const distElements = (a: HTMLElement, b: HTMLElement) => dist(elementPos(a), elementPos(b));

export const findClosestIdx = (p: P, pnts: P[]): number | undefined => {
let d = Infinity,
best: number | undefined = undefined;
for (const [idx, o] of pnts.entries()) {
const dt = dist(p, o);
if (dt < d) (d = dt), (best = idx);
}
return best;
};

export const findIdx = <Item extends Identifiable>(arr: Item[], id: Id) =>
arr.findIndex((item) => item.id === id);

export const moveByIdx = <T>(arr: T[], src: number, trg: number) =>
arr.splice(trg, 0, arr.splice(src, 1)[0]);
10 changes: 6 additions & 4 deletions src/lib/interfaces/strings.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
export type Text = string | number | bigint;
import type { Identifiable } from '$lib/grabs/utils';

export interface BoxState {
export type Stringable = string | number | bigint;

export interface BoxState<Text extends Stringable> extends Identifiable {
text: Text;
highlight?: boolean;
color?: 'red' | 'green' | 'yellow';
}

export interface StringMatchingState {
text: BoxState[];
pattern: BoxState[];
text: BoxState<string>[];
pattern: BoxState<string>[];
shift: number; // how much to shift the pattern (index in text)
focus: number; // index of pattern to be at the center of the view
}

0 comments on commit 6fcefc7

Please sign in to comment.