feat: add annotation mode
This commit is contained in:
@ -6,10 +6,12 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import GameCommands from './GameCommands.svelte';
|
import GameCommands from './GameCommands.svelte';
|
||||||
import SumIndicator from './SumIndicator.svelte';
|
import SumIndicator from './SumIndicator.svelte';
|
||||||
|
import Switch from './Switch.svelte';
|
||||||
|
|
||||||
let selectedI: number | undefined = undefined;
|
let selectedI: number | undefined = undefined;
|
||||||
let selectedJ: number | undefined = undefined;
|
let selectedJ: number | undefined = undefined;
|
||||||
let status: 'incomplete' | 'error' | 'won' = 'incomplete';
|
let status: 'incomplete' | 'error' | 'won' = 'incomplete';
|
||||||
|
let valueMode: 'Write' | 'Annotation' = 'Write';
|
||||||
|
|
||||||
const onSelect = (i: number, j: number) => () => {
|
const onSelect = (i: number, j: number) => () => {
|
||||||
if (i === selectedI && j === selectedJ) {
|
if (i === selectedI && j === selectedJ) {
|
||||||
@ -23,6 +25,7 @@
|
|||||||
|
|
||||||
const onSetValue = (number: number) => {
|
const onSetValue = (number: number) => {
|
||||||
if (selectedI !== undefined && selectedJ !== undefined) {
|
if (selectedI !== undefined && selectedJ !== undefined) {
|
||||||
|
if (valueMode === 'Write') {
|
||||||
board.grid[selectedI][selectedJ] = number;
|
board.grid[selectedI][selectedJ] = number;
|
||||||
if (board.isComplete()) {
|
if (board.isComplete()) {
|
||||||
status = board.isValid() ? 'won' : 'error';
|
status = board.isValid() ? 'won' : 'error';
|
||||||
@ -32,12 +35,22 @@
|
|||||||
selectedJ = undefined;
|
selectedJ = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (number) {
|
||||||
|
const cell = board.annotations[selectedI][selectedJ];
|
||||||
|
if (!cell.includes(number)) board.annotations[selectedI][selectedJ] = [...cell, number];
|
||||||
|
} else {
|
||||||
|
board.annotations[selectedI][selectedJ] = [];
|
||||||
|
}
|
||||||
|
console.log(board);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onGameCommand = (mode: string) => {
|
const onGameCommand = (mode: string) => {
|
||||||
if (mode === 'reset') {
|
if (mode === 'reset') {
|
||||||
board.reset();
|
board.reset();
|
||||||
|
board.grid = board.grid; // to trigger reload
|
||||||
} else {
|
} else {
|
||||||
board = new Board();
|
board = new Board();
|
||||||
if (mode === 'easy') board.prepare(Difficulty.Easy);
|
if (mode === 'easy') board.prepare(Difficulty.Easy);
|
||||||
@ -64,6 +77,7 @@
|
|||||||
{#each row as cell, j}
|
{#each row as cell, j}
|
||||||
<Cell
|
<Cell
|
||||||
value={cell}
|
value={cell}
|
||||||
|
annotations={board.annotations[i][j]}
|
||||||
onclick={onSelect(i, j)}
|
onclick={onSelect(i, j)}
|
||||||
selected={selectedI === i && selectedJ === j}
|
selected={selectedI === i && selectedJ === j}
|
||||||
disabled={!board.lockedCells[i][j]}
|
disabled={!board.lockedCells[i][j]}
|
||||||
@ -76,4 +90,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center py-8">
|
||||||
|
<Switch bind:value={valueMode} design="multi" options={['Annotation', 'Write']} />
|
||||||
|
</div>
|
||||||
<ValueSelector onclick={onSetValue} used={board.getPlacedNumbers()} />
|
<ValueSelector onclick={onSetValue} used={board.getPlacedNumbers()} />
|
||||||
|
@ -1,14 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { value, selected, disabled, onclick } = $props();
|
let {
|
||||||
|
value,
|
||||||
|
annotations,
|
||||||
|
selected,
|
||||||
|
disabled,
|
||||||
|
onclick
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
annotations: number[];
|
||||||
|
selected: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
onclick: () => void;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="aspect-square rounded-4xl border"
|
class="aspect-square rounded-4xl border border-gray-200"
|
||||||
class:bg-amber-600={selected}
|
class:bg-cyan-500={selected}
|
||||||
class:bg-gray-200={disabled}
|
class:bg-gray-200={disabled}
|
||||||
class:cursor-pointer={!disabled}
|
class:cursor-pointer={!disabled}
|
||||||
{onclick}
|
{onclick}
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
{value || '-'}
|
<div class="flex items-center justify-center">
|
||||||
|
{#if value}
|
||||||
|
<div>{value || ' '}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap justify-center space-y-2 space-x-2 md:flex-col">
|
||||||
|
{#each annotations as nb (nb)}
|
||||||
|
<div
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-full bg-emerald-300 p-1"
|
||||||
|
class:bg-white={selected}
|
||||||
|
>
|
||||||
|
{nb}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
263
src/lib/Switch.svelte
Normal file
263
src/lib/Switch.svelte
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// copy from https://svelte.dev/playground/d65a4e9f0ae74d1eb1b08d13e428af32?version=5.23.0
|
||||||
|
|
||||||
|
// based on suggestions from:
|
||||||
|
// Inclusive Components by Heydon Pickering https://inclusive-components.design/toggle-button/
|
||||||
|
// On Designing and Building Toggle Switches by Sara Soueidan https://www.sarasoueidan.com/blog/toggle-switch-design/
|
||||||
|
// and this example by Scott O'hara https://codepen.io/scottohara/pen/zLZwNv
|
||||||
|
|
||||||
|
export let label = '';
|
||||||
|
export let design = 'inner label';
|
||||||
|
export let options: string[] = [];
|
||||||
|
export let fontSize = 16;
|
||||||
|
export let value = 'on';
|
||||||
|
|
||||||
|
let checked = true;
|
||||||
|
|
||||||
|
const uniqueID = Math.floor(Math.random() * 100);
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
const state = target.getAttribute('aria-checked');
|
||||||
|
|
||||||
|
checked = state === 'true' ? false : true;
|
||||||
|
|
||||||
|
value = checked === true ? 'on' : 'off';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if design == 'inner'}
|
||||||
|
<div class="s s--inner">
|
||||||
|
<span id={`switch-${uniqueID}`}>{label}</span>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-labelledby={`switch-${uniqueID}`}
|
||||||
|
on:click={handleClick}
|
||||||
|
>
|
||||||
|
<span>on</span>
|
||||||
|
<span>off</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if design == 'slider'}
|
||||||
|
<div class="s s--slider" style="font-size:{fontSize}px">
|
||||||
|
<span id={`switch-${uniqueID}`}>{label}</span>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-labelledby={`switch-${uniqueID}`}
|
||||||
|
on:click={handleClick}
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="s s--multi">
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
class="group-container"
|
||||||
|
aria-labelledby={`label-${uniqueID}`}
|
||||||
|
style="font-size:{fontSize}px"
|
||||||
|
id={`group-${uniqueID}`}
|
||||||
|
>
|
||||||
|
<div class="legend" id={`label-${uniqueID}`}>{label}</div>
|
||||||
|
{#each options as option}
|
||||||
|
<input type="radio" id={`${option}-${uniqueID}`} value={option} bind:group={value} />
|
||||||
|
<label for={`${option}-${uniqueID}`}>
|
||||||
|
{option}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
@reference "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--accent-color: theme(colors.emerald.400);
|
||||||
|
--gray: #ccc;
|
||||||
|
}
|
||||||
|
/* Inner Design Option */
|
||||||
|
.s--inner button {
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid var(--gray);
|
||||||
|
}
|
||||||
|
[role='switch'][aria-checked='true'] :first-child,
|
||||||
|
[role='switch'][aria-checked='false'] :last-child {
|
||||||
|
display: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--inner button span {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--inner button:focus {
|
||||||
|
outline: var(--accent-color) solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider Design Option */
|
||||||
|
|
||||||
|
.s--slider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button {
|
||||||
|
width: 3em;
|
||||||
|
height: 1.6em;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 0 0 0.5em;
|
||||||
|
background: var(--gray);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
background: #fff;
|
||||||
|
top: 0.13em;
|
||||||
|
right: 1.5em;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button[aria-checked='true'] {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button[aria-checked='true']::before {
|
||||||
|
transform: translateX(1.3em);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button:focus {
|
||||||
|
box-shadow: 0 0px 0px 1px var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multi Design Option */
|
||||||
|
|
||||||
|
/* Based on suggestions from Sara Soueidan https://www.sarasoueidan.com/blog/toggle-switch-design/
|
||||||
|
and this example from Scott O'hara https://codepen.io/scottohara/pen/zLZwNv */
|
||||||
|
|
||||||
|
.s--multi .group-container {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .s--multi legend {
|
||||||
|
font-size: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.s--multi label {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.6;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi input {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi label:first-of-type {
|
||||||
|
padding-right: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi label:last-child {
|
||||||
|
margin-left: -5em;
|
||||||
|
padding-left: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi:focus-within label:first-of-type:after {
|
||||||
|
box-shadow: 0 0px 8px var(--accent-color);
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* making the switch UI. */
|
||||||
|
.s--multi label:first-of-type:before,
|
||||||
|
.s--multi label:first-of-type:after {
|
||||||
|
content: '';
|
||||||
|
height: 1.25em;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi label:first-of-type:before {
|
||||||
|
border-radius: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
background: #fff;
|
||||||
|
top: 0.2em;
|
||||||
|
right: 1.2em;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi label:first-of-type:after {
|
||||||
|
background: var(--accent-color);
|
||||||
|
border-radius: 1em;
|
||||||
|
margin: 0 1em;
|
||||||
|
transition: background 0.2s ease-in-out;
|
||||||
|
width: 3em;
|
||||||
|
height: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi input:first-of-type:checked ~ label:first-of-type:after {
|
||||||
|
background: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi input:first-of-type:checked ~ label:first-of-type:before {
|
||||||
|
transform: translateX(-1.4em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi input:last-of-type:checked ~ label:last-of-type {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--multi input:focus {
|
||||||
|
box-shadow: 0 0px 8px var(--accent-color);
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* gravy */
|
||||||
|
|
||||||
|
/* Inner Design Option */
|
||||||
|
[role='switch'][aria-checked='true'] :first-child,
|
||||||
|
[role='switch'][aria-checked='false'] :last-child {
|
||||||
|
border-radius: 0.25em;
|
||||||
|
background: var(--accent-color);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--inner button:focus {
|
||||||
|
box-shadow: 0 0px 8px var(--accent-color);
|
||||||
|
border-radius: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider Design Option */
|
||||||
|
.s--slider button {
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button::before {
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s--slider button:focus {
|
||||||
|
box-shadow: 0 0px 8px var(--accent-color);
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,7 +4,7 @@
|
|||||||
const numbers = Array.from({ length: 9 }, (_, i) => i + 1);
|
const numbers = Array.from({ length: 9 }, (_, i) => i + 1);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center space-x-2 py-8">
|
<div class="flex flex-wrap justify-center space-x-2">
|
||||||
{#each numbers as number (number)}
|
{#each numbers as number (number)}
|
||||||
<button
|
<button
|
||||||
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
|
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
|
||||||
@ -18,6 +18,6 @@
|
|||||||
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
|
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
|
||||||
onclick={() => onclick()}
|
onclick={() => onclick()}
|
||||||
>
|
>
|
||||||
sup
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,6 @@ function shuffleArray<T>(array: Array<T>): void {
|
|||||||
|
|
||||||
function getNumberToKeepFromDifficulty(difficulty: Difficulty) {
|
function getNumberToKeepFromDifficulty(difficulty: Difficulty) {
|
||||||
const rules = {
|
const rules = {
|
||||||
// 0: 8,
|
|
||||||
0: 3,
|
0: 3,
|
||||||
1: 2,
|
1: 2,
|
||||||
2: 1,
|
2: 1,
|
||||||
@ -29,6 +28,7 @@ function getNumberToKeepFromDifficulty(difficulty: Difficulty) {
|
|||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
grid: Case[][];
|
grid: Case[][];
|
||||||
|
annotations: number[][][];
|
||||||
verticalSum: number[];
|
verticalSum: number[];
|
||||||
horizontalSum: number[];
|
horizontalSum: number[];
|
||||||
lockedCells: boolean[][];
|
lockedCells: boolean[][];
|
||||||
@ -38,6 +38,7 @@ export class Board {
|
|||||||
shuffleArray(numbers);
|
shuffleArray(numbers);
|
||||||
|
|
||||||
this.grid = [];
|
this.grid = [];
|
||||||
|
this.annotations = [];
|
||||||
this.verticalSum = new Array(3).fill(0);
|
this.verticalSum = new Array(3).fill(0);
|
||||||
this.horizontalSum = [];
|
this.horizontalSum = [];
|
||||||
this.lockedCells = [];
|
this.lockedCells = [];
|
||||||
@ -49,6 +50,7 @@ export class Board {
|
|||||||
this.verticalSum[j] += row[j];
|
this.verticalSum[j] += row[j];
|
||||||
}
|
}
|
||||||
this.grid.push(row);
|
this.grid.push(row);
|
||||||
|
this.annotations.push([[], [], []]);
|
||||||
this.horizontalSum[i] = row.reduce((partialSum, a) => partialSum + a, 0);
|
this.horizontalSum[i] = row.reduce((partialSum, a) => partialSum + a, 0);
|
||||||
|
|
||||||
this.lockedCells[i] = [false, false, false];
|
this.lockedCells[i] = [false, false, false];
|
||||||
@ -100,10 +102,10 @@ export class Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
console.log('ici');
|
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
for (let j = 0; j < 3; j++) {
|
for (let j = 0; j < 3; j++) {
|
||||||
if (this.lockedCells[i][j]) this.grid[i][j] = undefined;
|
if (this.lockedCells[i][j]) this.grid[i][j] = undefined;
|
||||||
|
this.annotations[i][j] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,6 @@
|
|||||||
|
|
||||||
<h1 class="mb-5 text-center text-6xl font-medium">Fubuki</h1>
|
<h1 class="mb-5 text-center text-6xl font-medium">Fubuki</h1>
|
||||||
|
|
||||||
|
<div class="mx-2">
|
||||||
<Board />
|
<Board />
|
||||||
|
</div>
|
||||||
|
Reference in New Issue
Block a user