Made responsive

This commit is contained in:
Daniel Ledda
2021-07-06 15:01:43 +02:00
parent 97cb0bc550
commit 2d741b9740
20 changed files with 372 additions and 123 deletions

View File

@@ -13,7 +13,7 @@ import {
totalVolume
} from "./store";
const worker = new Worker('./solver/main.js', {type: 'module'});
const worker = new Worker('./worker.js', {type: 'module'});
async function respondWasm(event: MessageEvent) {
solutions.set(event.data.map((wasmSolution) => {
const solnObj = new SomaSolution(somaDimX.currentVal(), somaDimY.currentVal(), somaDimZ.currentVal());

View File

@@ -26,7 +26,14 @@ export const isMinPolycubes = derived(
($polycubes: VoxelSpaceBigInt[]) => $polycubes.length <= 1
);
export const examples = [
type Save = {
name: string,
dimX: number,
dimY: number,
dimZ: number,
cubes: {space: bigint | string, color: string}[],
};
const builtInExamples: Save[] = [
{
name: "Standard Soma Cube",
dimX: 3,
@@ -114,17 +121,34 @@ export const examples = [
{space: 120n, color: "#0000ff"},
],
},
].concat(deserealiseSaves());
];
function deserealiseSaves() {
export const examples = writable(builtInExamples.concat(deserealiseSaves()));
function deserealiseSaves(): Save[] {
return localStorage.getItem("saves")?.split("@").map(save => JSON.parse(save)) ?? [];
}
export function serialiseCurrentInput() {
function serialiseCurrentInput(): Save {
return {
name: "",
dimX: somaDimX.currentVal(),
dimY: somaDimY.currentVal(),
dimZ: somaDimZ.currentVal(),
cubes: polycubes.currentVal().map(cube => ({space: cube.getRaw().toString(), color: cube.getColor()})),
};
}
export function save(name: string) {
const save = serialiseCurrentInput();
save.name = name;
const saveString = JSON.stringify(save);
let oldSaves = localStorage.getItem("saves");
if (oldSaves !== null) {
oldSaves += "@";
} else {
oldSaves = "";
}
localStorage.setItem("saves", oldSaves + saveString);
examples.update(examples => examples.concat(save));
}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let onClick: () => void = () => {};
export let tooltip: string | null = null;
export let disabled: boolean = false;
export let text: string;
</script>
<button
on:click={onClick}
title="{tooltip}"
disabled={disabled}>
{text}
</button>
<style>
button {
width: auto;
color: white;
background-color: #ff3e00;
border-radius: 0.5em;
border-style: none;
margin: 0;
cursor: pointer;
}
button:hover {
background-color: #ff6b3e;
}
button:disabled {
color: #999999;
background-color: #a36754;
}
</style>

View File

@@ -10,13 +10,14 @@
<div class="sidebarContainer">
<Sidebar />
</div>
<div class="solutionBodyContainer">
<div class="stage">
<Stage scene="{scene}" />
</div>
</main>
<style>
main {
position: relative;
display: flex;
height: 100%;
margin: 0;
@@ -25,28 +26,41 @@
background-color: black;
margin: 0;
padding: 0;
width: 20%;
max-width: 20%;
height: 100%;
display: inline-block;
}
.solutionBodyContainer {
.stage {
flex: 1;
background-color: grey;
margin: 0;
padding: 0;
width: 80%;
height: 100%;
width: 80%;
display: inline-block;
}
:global(body) {
color: white;
background: #333333;
}
@media(max-width: 1200px) {
.solutionBodyContainer {
width: calc(100% - 18em);
}
@media(width: 1200px) {
.sidebarContainer {
width: 18em;
}
.stage {
width: calc(80% - 18em);
}
}
@media(max-width: 1024px) {
.stage {
left: 2em;
position: absolute;
width: calc(100% - 2em);
}
.sidebarContainer {
position: absolute;
max-width: 100%;
z-index: 1;
}
}
</style>

View File

@@ -122,7 +122,7 @@
}
.colorPickerBtn {
align-self: center;
background-image: url("../resources/ColorWheel.png");
background-image: url("./ColorWheel.png");
background-size: cover;
width: 1.5em;
height: 1.5em;

View File

@@ -26,9 +26,9 @@
margin: auto;
}
.container {
flex: 1 1 auto;
overflow-x: scroll;
display: flex;
width: 100%;
flex-flow: row;
margin: auto;
}

View File

@@ -1,67 +1,65 @@
<script lang="ts">
import List from "./List.svelte";
import {polycubes, examples, serialiseCurrentInput} from "../store";
import {polycubes, examples, save} from "../store";
import VoxelSpaceBigInt from "../VoxelSpaceBigInt";
import ActionButton from "./ActionButton.svelte";
const placeholder = "Give your puzzle a name";
let untouchedInput = true;
let currentName = placeholder;
let lastClickedExample = 0;
let untouchedInput: boolean = true;
let currentName: string = placeholder;
let lastClickedExample: number | null = null;
function hydrateExample(exNo: number) {
const example = examples[exNo];
const example = $examples[exNo];
polycubes.setCubes(example.cubes.map((cube, i) => new VoxelSpaceBigInt({
id: i,
dims: [example.dimX, example.dimY, example.dimZ],
space: cube.space,
space: BigInt(cube.space),
color: cube.color,
cullEmpty: false,
})));
lastClickedExample = exNo;
}
function checkUntouched() {
if (currentName === "") {
untouchedInput = true;
} else {
untouchedInput = false;
}
}
function onInput(e: InputEvent) {
currentName = (e.target as HTMLInputElement).value;
checkUntouched();
}
function onFocusText(e: FocusEvent) {
if (untouchedInput) {
currentName = "";
untouchedInput = false;
}
}
function onBlurText(e: FocusEvent) {
if (currentName === "") {
if (untouchedInput) {
currentName = placeholder;
untouchedInput = true;
}
}
function save() {
const save = serialiseCurrentInput();
save["name"] = currentName;
const saveString = JSON.stringify(save);
let oldSaves = window.localStorage.getItem("saves");
if (oldSaves !== null) {
oldSaves += "@";
} else {
oldSaves = "";
}
window.localStorage.setItem("saves", oldSaves + saveString);
}
</script>
<div class="container">
<List
defaultText="No examples found..."
items="{examples.map(example => example.name)}"
items="{$examples.map(example => example.name)}"
activeItem={lastClickedExample}
onClick={(i) => hydrateExample(i)}
/>
</div>
<div class="flex">
<button on:click={save}>Save as...</button>
<div class="save">
<ActionButton
onClick={() => save(currentName)}
text={"Save as..."}
disabled={untouchedInput}/>
<input
class:untouchedInput
value="{currentName}"
@@ -75,11 +73,22 @@
.untouchedInput {
color: grey;
}
button {
.save {
white-space: nowrap;
}
.flex {
display: flex;
flex-wrap: wrap;
row-gap: 1em;
align-items: center;
justify-content: space-evenly;
margin-top: 1em;
}
input {
flex-basis: 10em;
flex: 1;
max-width: 100%;
min-width: 50%;
margin: 0 0 0 0.5em;
width: 100%;
}
.container {
height: 10em;

View File

@@ -32,9 +32,6 @@
downDisabled="{$somaDimZ <= PolycubeStore.MIN_DIMS}"
down="{() => somaDimZ.set($somaDimZ - 1)}"
/>
{#if $totalVolume > 32}
<p class="warn">The total number of units exceeds 32. Attempting to solve puzzles with more than 32 units results in significantly slower computation time.</p>
{/if}
</div>
<div class="option">
@@ -49,7 +46,4 @@
</div>
<style>
.warn {
color: red;
}
</style>

View File

@@ -29,6 +29,10 @@
list-style: none;
height: 2em;
line-height: 2em;
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
ul {
position: absolute;

View File

@@ -7,32 +7,46 @@
import ExamplesList from "./ExamplesList.svelte";
import Tabs from "./Tabs.svelte";
import SolveButton from "./SolveButton.svelte";
import ActionButton from "./ActionButton.svelte";
let hidden: boolean = true;
</script>
<div class="container">
<div class="title">
<img class="logo" src="../favicon.png"/><h1>Somaesque</h1>
</div>
<div class="widgets">
<div>
<Tabs
selectedTab={"Parameters"}
tabs={{
"Parameters": InputParameters,
"Examples": ExamplesList,
}}/>
<div class="controls" class:hidden>
<div class="title">
<img class="logo" src="./resources/favicon.png"/>
<h1>Somaesque</h1>
</div>
<div>
<SolveButton/>
<div class="widgets">
<div>
<Tabs
selectedTab={"Parameters"}
tabs={{
"Parameters": InputParameters,
"Examples": ExamplesList,
}}/>
</div>
<div>
<SolveButton/>
</div>
</div>
<h3>Solutions: {$solutions.length}</h3>
<div class="solns">
<SolutionList/>
</div>
</div>
<h3>Solutions: {$solutions.length}</h3>
<div class="solns">
<SolutionList/>
<div class="showHideBtn">
<ActionButton text={hidden ? ">" : "<"} onClick={() => {hidden = !hidden}}/>
</div>
</div>
<style>
.container {
display: flex;
height: 100%;
flex-direction: row;
}
.title {
display: flex;
align-items: center;
@@ -41,12 +55,16 @@
margin-right: 1em;
display: inline-block;
height: 3em;
align-self: center;
background-image: url("../resources/favicon.png");
background-size: cover;
width: 3em;
}
.container {
.controls {
display: flex;
align-items: center;
flex-direction: column;
height: 100vh;
height: 100%;
overflow: hidden;
background-color: #333333;
padding: 1em;
@@ -76,4 +94,34 @@
font-size: 3em;
font-weight: 100;
}
.showHideBtn {
background-color: #333333;
text-align: center;
line-height: 100%;
width: 2em;
height: 100%;
padding: 0.25em;
flex: 0 0 auto;
display: none;
}
@media(max-width: 1600px) {
h1 {
font-size: 2em;
}
}
@media(max-width: 1024px) {
.controls {
overflow: scroll;
padding-right: 0;
}
.showHideBtn {
display: inline-block;
}
.hidden {
display: none;
}
.container.hidden {
pointer-events: none;
}
}
</style>

View File

@@ -20,5 +20,6 @@
<style>
.container {
height: 100%;
min-height: 10em;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import {polycubes, solving, somaDimX, somaDimY, somaDimZ, totalVolume} from "../store";
import {solve} from "../solve";
import ActionButton from "./ActionButton.svelte";
$: cubes = $polycubes;
let noEmpties: boolean;
@@ -26,29 +27,40 @@
}
</script>
<button
class="solve"
on:click={solve}
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
disabled="{$solving || !readyToSolve}">
{$solving ? "Solving..." : "Solve!"}
</button>
<div class="container">
<div class="solve">
<ActionButton
onClick={solve}
tooltip={genTooltip(enoughSubcubes, noEmpties, size)}
disabled={$solving || !readyToSolve}
text={$solving ? "Solving..." : "Solve!"}/>
</div>
{#if $totalVolume > 32}
<p class="warn">The total number of units exceeds 32. Attempting to solve puzzles with more than 32 units results in significantly slower computation time.</p>
{/if}
</div>
<style>
button.solve {
width: auto;
color: white;
background-color: #ff3e00;
font-size: 2em;
border-radius: 0.5em;
border-style: none;
margin: 0;
cursor: pointer;
.container {
display: flex;
flex-direction: row;
align-content: center;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
}
button.solve:disabled {
.warn {
flex-basis: 7em;
flex: 1;
min-width: 7em;
margin-left: 1em;
color: red;
text-align: left;
}
.solve {
height: min-content;
width: auto;
color: #999999;
background-color: #a36754;
font-size: 2em;
margin: 0;
}
</style>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import SolutionViewer from "./SolutionViewer.svelte";
import ThreeDee from "./ThreeDee.svelte";
import CubeInputSet from "./CubeInputSet.svelte";
import PolycubeScene from "./threedee/PolycubeScene";
@@ -12,7 +12,7 @@
}
const queryListWidth = window.matchMedia("(max-width: 1200px)");
const queryListHeight = window.matchMedia("(max-height: 920px)");
const queryListHeight = window.matchMedia("(max-height: 900px)");
queryListWidth.addEventListener("change", onMediaChange);
queryListHeight.addEventListener("change", onMediaChange);
onMediaChange();
@@ -27,11 +27,11 @@
{#if showInput}
<CubeInputSet/>
{:else}
<SolutionViewer scene="{scene}"/>
<ThreeDee scene="{scene}"/>
{/if}
{:else}
<CubeInputSet/>
<SolutionViewer scene="{scene}"/>
<ThreeDee scene="{scene}"/>
{/if}
</div>
@@ -59,9 +59,9 @@
border-width: 1px 0 0 0;
}
.viewport {
overflow: scroll;
display: flex;
height: 100%;
width: 100%;
align-content: center;
justify-content: flex-start;
flex-direction: column;

View File

@@ -3,6 +3,7 @@
import {onMount} from "svelte";
import {polycubes, solutions, activeSolution, showingSolution} from "../store";
import Solution2D from "./Solution2D.svelte";
import CubeInput from "./CubeInput.svelte";
export let scene: PolycubeScene;
const selectedStore = polycubes.selected();
@@ -10,17 +11,32 @@
$: cube = $polycubes[selectedCube];
$: soln = $solutions[$activeSolution];
let el: HTMLDivElement;
let containerEl: HTMLDivElement;
let loaded: boolean = false;
const canvasStyle: Partial<CSSStyleDeclaration> = {
borderRadius: "1em",
};
function updateDims() {
if (window.innerWidth < 1200) {
if (containerEl.clientHeight < containerEl.clientWidth * (3 / 4)) {
scene.updateDims({width: (containerEl.clientHeight - 50) * (4 / 3), height: containerEl.clientHeight - 50});
} else {
scene.updateDims({width: containerEl.scrollWidth - 50, height: (containerEl.scrollWidth - 50) * (3 / 4)});
}
} else {
scene.resetDims();
}
}
onMount(() => {
scene.onLoaded(() => {
scene.mount(el);
Object.assign((el.children.item(0) as HTMLElement).style, canvasStyle);
loaded = true;
window.addEventListener("resize", () => updateDims());
updateDims();
});
});
@@ -35,11 +51,15 @@
}
</script>
<div class="container">
{#if $activeSolution !== null}
<div class="container" bind:this={containerEl}>
{#if $showingSolution && ($activeSolution !== null)}
<div class="soln2d-container">
<Solution2D/>
</div>
{:else if !$showingSolution}
<div class="soln2d-container">
<CubeInput cubeNo="{selectedCube}"/>
</div>
{/if}
<div class="stage" bind:this={el}></div>
</div>
@@ -49,6 +69,11 @@
flex: 0 1 auto;
display: inline-block;
}
@media (max-width: 1200px) {
.soln2d-container {
display: none;
}
}
.container {
flex: 1 1 auto;
display: flex;
@@ -57,5 +82,6 @@
justify-content: space-evenly;
text-align: center;
align-items: center;
overflow: hidden;
}
</style>

View File

@@ -21,8 +21,10 @@ export default class PolycubeScene {
private canvas: HTMLCanvasElement;
private loadedCb: () => void = () => {};
private loaded: boolean = false;
private windowDims: {width: number, height: number};
constructor() {
constructor(windowDims?: {width: number, height: number}) {
this.windowDims = windowDims ?? {width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT};
this.init().then(() => this.loadedCb()).catch(e => console.log(e));
}
@@ -56,12 +58,24 @@ export default class PolycubeScene {
}
mount(el: HTMLDivElement) {
this.canvas.width = DEFAULT_WIDTH;
this.canvas.height = DEFAULT_HEIGHT;
this.updateDims(this.windowDims);
el.append(this.canvas);
}
updateDims(windowDims: {width: number, height: number}) {
this.windowDims.width = windowDims.width;
this.windowDims.height = windowDims.height;
this.canvas.width = this.windowDims.width;
this.canvas.height = this.windowDims.height;
this.camera.aspect = this.canvas.width / this.canvas.height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.canvas.width, this.canvas.height);
el.append(this.canvas);
}
resetDims() {
this.windowDims.width = DEFAULT_WIDTH;
this.windowDims.height = DEFAULT_HEIGHT;
this.updateDims(this.windowDims);
}
onLoaded(cb: () => void) {

View File

@@ -1,4 +1,5 @@
import type * as THREE from 'three';
import {MOUSE} from "three";
interface Fliable {
flyBy(factor: number);
@@ -17,6 +18,10 @@ export default class RotationControls {
private start: THREE.Euler;
private fliables: Fliable[];
private hovered: boolean = false;
private scrolling: boolean = false;
private lastTouch: {x: number, y: number} = {x: 0, y: 0};
private lastScroll1: {x: number, y: number} = {x: 0, y: 0};
private lastScroll2: {x: number, y: number} = {x: 0, y: 0};
constructor(object: THREE.Object3D, fliables: Fliable[], camera: THREE.Camera, element: HTMLCanvasElement) {
this.object = object;
@@ -28,23 +33,29 @@ export default class RotationControls {
this.xAxis = this.xAxis.clone().cross(this.yAxis.clone());
this.start = this.object.rotation.clone();
this.element.addEventListener('touchstart', (ev) => this.handleTouchStart(ev));
this.element.addEventListener("touchcancel", (ev) => this.handleTouchEnd(ev));
window.addEventListener('touchmove', (ev) => this.handleTouchMove(ev));
window.addEventListener('touchend', (ev) => this.handleTouchEnd(ev));
this.element.addEventListener('wheel', (ev) => this.handleScroll(ev));
this.element.addEventListener('mouseover', () => this.hovered = true);
this.element.addEventListener('mouseout', () => this.hovered = false);
this.element.addEventListener('wheel', (ev) => this.handleScroll(ev));
this.element.addEventListener('mousedown', (event) => {
if (event.button === 1) {
this.object.setRotationFromEuler(this.start);
}
if (!this.dragging) {
this.lastX = event.x;
this.lastY = event.y;
this.dragging = true;
}
});
this.element.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
window.addEventListener('mousemove', (ev) => this.handleMove(ev));
window.addEventListener('mouseup', () => this.dragging = false);
}
private handleMouseDown(event: MouseEvent) {
if (event.button === 1) {
this.object.setRotationFromEuler(this.start);
}
if (!this.dragging) {
this.lastX = event.x;
this.lastY = event.y;
this.dragging = true;
}
}
private handleMove(event: MouseEvent) {
if (this.dragging) {
const xDiff = event.movementX * RotationControls.ROTATION_FACTOR;
@@ -63,6 +74,56 @@ export default class RotationControls {
}
}
private handleTouchMove(event: TouchEvent) {
if (this.dragging) {
const newTouchX = event.touches.item(0).clientX;
const newTouchY = event.touches.item(0).clientY;
const touchDiffX = newTouchX - this.lastTouch.x;
const touchDiffY = newTouchY - this.lastTouch.y;
const xDiff = touchDiffX * RotationControls.ROTATION_FACTOR;
const yDiff = touchDiffY * RotationControls.ROTATION_FACTOR;
this.object.rotateOnAxis(this.yAxis, xDiff);
this.object.rotateOnWorldAxis(this.xAxis, yDiff);
this.lastTouch.x = newTouchX;
this.lastTouch.y = newTouchY;
} else if (this.scrolling) {
if (this.flyingEnabled && this.hovered) {
const newTouchX1 = event.touches.item(0).clientX;
const newTouchX2 = event.touches.item(1).clientX;
const newTouchY1 = event.touches.item(0).clientY;
const newTouchY2 = event.touches.item(1).clientY;
const lastDist = Math.sqrt((this.lastScroll1.x - this.lastScroll2.x) ** 2 + (this.lastScroll1.y - this.lastScroll2.y) ** 2);
const newDist = Math.sqrt((newTouchX1 - newTouchX2) ** 2 + (newTouchY1 - newTouchY2) ** 2)
const delta = newDist - lastDist;
for (const fliable of this.fliables) {
const direction = delta / Math.abs(delta);
fliable.flyBy(direction / 10);
}
}
}
}
private handleTouchStart(event: TouchEvent) {
if (event.touches.length === 1) {
this.lastTouch.x = event.touches.item(0).clientX;
this.lastTouch.y = event.touches.item(0).clientY;
this.dragging = true;
} else if (event.touches.length === 2) {
this.lastScroll1.x = event.touches.item(0).clientX;
this.lastScroll1.y = event.touches.item(0).clientY;
this.lastScroll2.x = event.touches.item(1).clientX;
this.lastScroll2.y = event.touches.item(1).clientY;
this.scrolling = true;
}
this.hovered = true;
}
private handleTouchEnd(event: TouchEvent) {
this.dragging = false;
this.scrolling = false;
this.hovered = false;
}
enableFly() {
this.flyingEnabled = true;
}