visual layout adjustments

This commit is contained in:
2023-10-31 22:09:33 +01:00
parent a7b49de6ac
commit e4d51cd51d
10 changed files with 390 additions and 249 deletions

View File

@@ -1,36 +1,25 @@
name: CC1
col:
- gap: 156
- row:
- switch: { d: 30, e: 31, n: 32, w: 33, s: 34 }
- switch: { d: 25, e: 26, n: 27, w: 28, s: 29 }
- switch: { d: 40, e: 41, n: 42, w: 43, s: 44 }
- switch: { d: 20, e: 21, n: 22, w: 23, s: 24 }
- switch: { d: 35, e: 36, n: 37, w: 38, s: 39 }
- switch: { d: 15, e: 16, n: 17, w: 18, s: 19 }
- switch: { d: 60, w: 61, n: 62, e: 63, s: 64 }
- switch: { d: 65, w: 66, n: 67, e: 68, s: 69 }
- switch: { d: 80, w: 81, n: 82, e: 83, s: 84 }
- switch: { d: 70, w: 71, n: 72, e: 73, s: 74 }
- switch: { d: 85, w: 86, n: 87, e: 88, s: 89 }
- switch: { d: 75, w: 76, n: 77, e: 78, s: 79 }
- row:
- switch: { d: 10, e: 11, n: 12, w: 13, s: 14 }
- switch: { d: 55, w: 56, n: 57, e: 58, s: 59 }
- row:
- switch: { d: 5, e: 6, n: 7, w: 8, s: 9 }
- switch: { d: 50, w: 51, n: 52, e: 53, s: 54 }
- offset: [ 0, 0 ]
row:
- row:
- {d: 30, e: 31, n: 32, w: 33, s: 34}
- col:
- {d: 25, e: 26, n: 27, w: 28, s: 29}
- {d: 40, e: 41, n: 42, w: 43, s: 44}
- col:
- {d: 20, e: 21, n: 22, w: 23, s: 24}
- {d: 35, e: 36, n: 37, w: 38, s: 39}
- {d: 15, e: 16, n: 17, w: 18, s: 19}
- row:
- {d: 60, w: 61, n: 62, e: 63, s: 64}
- col:
- {d: 65, w: 66, n: 67, e: 68, s: 69}
- {d: 80, w: 81, n: 82, e: 83, s: 84}
- col:
- {d: 70, w: 71, n: 72, e: 73, s: 74}
- {d: 85, w: 86, n: 87, e: 88, s: 89}
- {d: 75, w: 76, n: 77, e: 78, s: 79}
- gap: 48
margin-top: -32
row:
- {d: 10, e: 11, n: 12, w: 13, s: 14}
- {d: 55, w: 56, n: 57, e: 58, s: 59}
- gap: 160
row:
- {d: 5, e: 6, n: 7, w: 8, s: 9}
- {d: 50, w: 51, n: 52, e: 53, s: 54}
- gap: 320
margin-top: -12
row:
- {d: 0, e: 1, n: 2, w: 3, s: 4}
- {d: 45, w: 46, n: 47, e: 48, s: 49}
- switch: { d: 0, e: 1, n: 2, w: 3, s: 4 }
- switch: { d: 45, w: 46, n: 47, e: 48, s: 49 }

View File

@@ -1,87 +1,87 @@
name: Lite
row:
- col:
- id: 53
- id: 54
- id: 55
- id: 56
- id: 57
- id: 58
- id: 59
- id: 60
- id: 61
- id: 62
- id: 63
- id: 64
- id: 65
- id: 66
col:
- row:
- key: 53
- key: 54
- key: 55
- key: 56
- key: 57
- key: 58
- key: 59
- key: 60
- key: 61
- key: 62
- key: 63
- key: 64
- key: 65
- key: 66
size: [ 2, 1 ]
- col:
- id: 39
- row:
- key: 39
size: [ 1.5, 1 ]
- id: 40
- id: 41
- id: 42
- id: 43
- id: 44
- id: 45
- id: 46
- id: 47
- id: 48
- id: 49
- id: 50
- id: 51
- id: 52
- key: 40
- key: 41
- key: 42
- key: 43
- key: 44
- key: 45
- key: 46
- key: 47
- key: 48
- key: 49
- key: 50
- key: 51
- key: 52
size: [ 1.5, 1 ]
- col:
- id: 26
- row:
- key: 26
size: [ 1.75, 1 ]
- id: 27
- id: 28
- id: 29
- id: 30
- id: 31
- id: 32
- id: 33
- id: 34
- id: 35
- id: 36
- id: 37
- id: 38
- key: 27
- key: 28
- key: 29
- key: 30
- key: 31
- key: 32
- key: 33
- key: 34
- key: 35
- key: 36
- key: 37
- key: 38
size: [ 2.25, 1 ]
- col:
- id: 12
- row:
- key: 12
size: [ 2, 1 ]
- id: 13
- id: 14
- id: 15
- id: 16
- id: 17
- id: 18
- id: 19
- id: 20
- id: 21
- id: 22
- id: 23
- id: 24
- id: 25
- col:
- id: 0
- id: 1
- key: 13
- key: 14
- key: 15
- key: 16
- key: 17
- key: 18
- key: 19
- key: 20
- key: 21
- key: 22
- key: 23
- key: 24
- key: 25
- row:
- key: 0
- key: 1
size: [ 1.25, 1 ]
- id: 2
- key: 2
size: [ 1.25, 1 ]
- id: 3
- key: 3
size: [ 2, 1 ]
- id: 4
- id: 5
- id: 6
- key: 4
- key: 5
- key: 6
size: [ 2, 1 ]
- id: 7
- key: 7
size: [ 1.25, 1 ]
- id: 8
- key: 8
size: [ 1.25, 1 ]
- id: 9
- id: 10
- id: 11
- key: 9
- key: 10
- key: 11

View File

@@ -1,24 +1,19 @@
<script lang="ts">
import rawLayout from "$lib/assets/layouts/lite.yml"
import rawLayout from "$lib/assets/layouts/cc1.yml"
import {compileLayout} from "$lib/serialization/visual-layout"
import type {VisualLayout, CompiledLayoutKey} from "$lib/serialization/visual-layout"
import {changes, layout} from "$lib/serial/connection"
import type {Change} from "$lib/serial/connection"
import {dev} from "$app/environment"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
import type {KeyInfo} from "$lib/serial/keymap-codes.js"
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
import {get} from "svelte/store"
import type {CharaLayout} from "$lib/serialization/layout"
import type {Writable} from "svelte/store"
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte"
import {getContext} from "svelte"
import type {VisualLayoutConfig} from "./visual-layout.js"
const scale = 50
export let inactiveScale = 0.6
export let inactiveOpacity = 0.4
export let strokeWidth = 1
export let margin = 5
export let fontSize = 9
export let iconFontSize = 14
export let activeLayer: number
const {scale, margin, strokeWidth, fontSize, iconFontSize} =
getContext<VisualLayoutConfig>("visual-layout-config")
const activeLayer = getContext<Writable<number>>("active-layer")
if (dev) {
// you have absolutely no idea what a difference this makes for performance
@@ -114,22 +109,12 @@
}
}
function getActions(layer: number, id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, boolean] {
const actionId = layout?.[layer][id]
const changedId = changes.findLast(it => it?.layout?.[layer]?.[id] !== undefined)?.layout?.[layer]?.[id]
if (changedId !== undefined) {
return [KEYMAP_CODES[changedId], true]
} else {
return [KEYMAP_CODES[actionId], false]
}
}
function edit(index: number) {
const keyInfo = layoutInfo.keys[index]
const clickedGroup = groupParent.children.item(index) as SVGGElement
const component = new ActionSelector({
target: document.body,
props: {currentAction: get(layout)[activeLayer][keyInfo.id]},
props: {currentAction: get(layout)[get(activeLayer)][keyInfo.id]},
})
const dialog = document.querySelector("dialog > div") as HTMLDivElement
const backdrop = document.querySelector("dialog") as HTMLDialogElement
@@ -167,7 +152,7 @@
component.$on("close", closed)
component.$on("select", ({detail}) => {
changes.update(changes => {
changes.push({layout: {[activeLayer]: {[keyInfo.id]: detail}}})
changes.push({layout: {[get(activeLayer)]: {[keyInfo.id]: detail}}})
return changes
})
closed()
@@ -183,114 +168,24 @@
<p>{layoutInfo.name}</p>
<svg viewBox="0 0 {layoutInfo.size[0] * scale} {layoutInfo.size[1] * scale}" bind:this={groupParent}>
{#each layoutInfo.keys as key, i}
{@const posX = key.pos[0] * scale}
{@const posY = key.pos[1] * scale}
{@const sizeX = key.size[0] * scale}
{@const sizeY = key.size[1] * scale}
{@const middleX = sizeX / 2}
{@const middleY = sizeY / 2}
<g
class="key-group"
<KeyboardKey
{i}
{key}
on:focusin={() => (focusKey = key)}
on:click={() => edit(i)}
on:keypress={({key}) => {
if (key === "Enter") {
edit(i)
}
}}
on:focusin={() => (focusKey = key)}
role="button"
tabindex={i + 1}
>
<rect
x={posX + margin}
y={posY + margin}
rx={margin}
width={sizeX - margin * 2}
height={sizeY - margin * 2}
stroke="currentcolor"
stroke-width={strokeWidth}
/>
{#each [1, 2, 0] as layer, i}
{@const [action, changed] = getActions(layer, key.id, $layout, $changes)}
{@const isActive = layer === activeLayer}
{@const direction = [
(middleX - margin * 3) / (i % 2 === 0 ? -1 : 1),
(middleY - margin * 3) / (i < 2 ? -1 : 1),
]}
{@const layerFontSize = action?.icon ? iconFontSize : fontSize}
<g
style="transform: {isActive
? `translate3d(0, 0, 0) scale(1)`
: `translate3d(${direction[0]}px, ${direction[1]}px, 0) scale(${inactiveScale})`}"
>
<text
fill={changed ? "var(--md-sys-color-primary)" : "currentcolor"}
text-anchor="middle"
alignment-baseline="central"
x={posX + middleX + (changed ? fontSize / 3 : 0)}
y={posY + middleY}
font-size={layerFontSize}
font-family={action?.icon ? "Material Symbols Rounded" : undefined}
opacity={isActive ? 1 : inactiveOpacity}
>
{action?.icon || action?.id || action?.code || `{${key.id}}`}
{#if changed}
<tspan font-weight="bold"></tspan>
{/if}
</text>
</g>
{/each}
</g>
/>
{/each}
</svg>
<style lang="scss">
$focus-transition: 10ms;
$transition: 200ms;
svg {
overflow: visible;
width: calc(min(100%, 35cm));
max-height: calc(100% - 170px);
}
text {
transition:
fill #{$focus-transition} ease,
opacity #{$transition} ease;
}
rect {
fill: var(--md-sys-color-background);
transition:
fill #{$focus-transition} ease,
stroke #{$focus-transition} ease,
fill-opacity #{$focus-transition} ease;
}
g {
transform-origin: center;
transform-box: fill-box;
transition:
fill #{$focus-transition} ease,
opacity #{$transition} ease,
transform #{$transition} ease;
}
.key-group:hover {
cursor: default;
opacity: 0.6;
transition: opacity #{$transition} ease;
}
.key-group:focus-within {
color: var(--md-sys-color-primary);
outline: none;
> rect {
outline: none;
fill: currentcolor;
fill-opacity: 0.2;
}
}
</style>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import {getActions} from "$lib/components/layout/get-actions.js"
import {changes, layout} from "$lib/serial/connection.js"
import {getContext} from "svelte"
import type {Writable} from "svelte/store"
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
const {fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize} =
getContext<VisualLayoutConfig>("visual-layout-config")
const activeLayer = getContext<Writable<number>>("active-layer")
export let key: CompiledLayoutKey
export let fontSizeMultiplier = 1
export let middle: [number, number]
export let pos: [number, number]
export let rotate: number
export let positions: [[number, number], [number, number], [number, number]]
</script>
{#each positions as position, layer}
{@const [action, changed] = getActions(layer, key.id, $layout, $changes)}
{@const isActive = layer === $activeLayer}
{@const direction = [(middle[0] - margin * 3) / position[0], (middle[1] - margin * 3) / position[1]]}
<text
fill={changed ? "var(--md-sys-color-primary)" : "currentcolor"}
font-weight={changed ? "bold" : ""}
text-anchor="middle"
alignment-baseline="central"
x={pos[0] + middle[0] + (changed ? fontSize / 3 : 0)}
y={pos[1] + middle[1]}
font-size={fontSizeMultiplier * (action.icon ? iconFontSize : fontSize)}
font-family={action.icon ? "Material Symbols Rounded" : undefined}
opacity={isActive ? 1 : inactiveOpacity}
style:scale={isActive ? 1 : inactiveScale}
style:translate={isActive ? "0 0" : `${direction[0]}px ${direction[1]}px`}
style:rotate="{rotate}rad"
>
{#if action.code !== 0}
{action.icon || action.id || `0x${action.code?.toString(16)}`}
{/if}
{#if changed}
<tspan></tspan>
{/if}
</text>
{/each}
<style lang="scss">
$focus-transition: 10ms;
$transition: 200ms;
text {
transform-origin: center;
transform-box: fill-box;
transition:
fill #{$focus-transition} ease,
opacity #{$transition} ease,
translate #{$transition} ease,
scale #{$transition} ease;
}
</style>

View File

@@ -0,0 +1,102 @@
<script lang="ts">
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
import {getContext} from "svelte"
import type {VisualLayoutConfig} from "./visual-layout.js"
import KeyText from "$lib/components/layout/KeyText.svelte"
const {scale, margin, strokeWidth} = getContext<VisualLayoutConfig>("visual-layout-config")
export let i: number
export let key: CompiledLayoutKey
$: posX = key.pos[0] * scale
$: posY = key.pos[1] * scale
$: sizeX = key.size[0] * scale
$: sizeY = key.size[1] * scale
</script>
<g class="key-group" on:click on:keypress on:focusin role="button" tabindex={i + 1}>
{#if key.shape === "square"}
<rect
x={posX + margin}
y={posY + margin}
rx={key.cornerRadius * scale}
width={sizeX - margin * 2}
height={sizeY - margin * 2}
stroke-width={strokeWidth}
/>
<KeyText
{key}
middle={[sizeX / 2, sizeY / 2]}
pos={[posX, posY]}
rotate={-key.rotate}
positions={[
[-1, 1],
[-1, -1],
[1, -1],
]}
/>
{:else if key.shape === "quarter-circle"}
{@const innerMargin = margin / 2}
{@const r1 = sizeX / 2 - margin}
{@const p1 = r1 - innerMargin}
{@const r2 = r1 - sizeY + innerMargin * 2}
{@const p2 = r2 - innerMargin}
{@const multiplier = 1.4}
<g style:transform="rotateZ({key.rotate}rad) translate({innerMargin}px, {innerMargin}px)">
<path
d="M{posX + p1},{posY} a{r1},{r1} 0 0,1 {-p1},{p1} l0,{-(p1 - p2)} a{r2},{r2} 0 0,0 {p2},{-p2}z"
/>
<KeyText
{key}
middle={[sizeY - margin * 2, sizeY - margin * 2]}
pos={[posX, posY]}
rotate={-key.rotate}
fontSizeMultiplier={multiplier}
positions={[
[-0.6, -0.6],
[0.6, -0.6],
[-0.6, 0.6],
]}
/>
</g>
{/if}
</g>
<style lang="scss">
$focus-transition: 10ms;
$transition: 200ms;
rect {
transform-origin: center;
transform-box: fill-box;
}
g {
transform-origin: top left;
transform-box: fill-box;
}
path,
rect {
fill: var(--md-sys-color-background);
fill-opacity: 0;
stroke: currentcolor;
}
g:hover {
cursor: default;
opacity: 0.6;
transition: opacity #{$transition} ease;
}
g:focus-within {
color: var(--md-sys-color-primary);
outline: none;
> path,
> rect {
fill: currentcolor;
fill-opacity: 0.2;
}
}
</style>

View File

@@ -3,9 +3,11 @@
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
import {action} from "$lib/title"
import GenericLayout from "$lib/components/layout/GenericLayout.svelte"
import {getContext} from "svelte"
import type {Writable} from "svelte/store"
$: device = $serialPort?.device ?? "ONE"
let activeLayer = 0
const activeLayer = getContext<Writable<number>>("active-layer")
const layers = [
["Numeric Layer", "123", 1],
@@ -20,8 +22,8 @@
<button
class="icon"
use:action={{title, shortcut: `alt+${value + 1}`}}
on:click={() => (activeLayer = value)}
class:active={activeLayer === value}
on:click={() => ($activeLayer = value)}
class:active={$activeLayer === value}
>
{icon}
</button>
@@ -29,7 +31,7 @@
</fieldset>
{#if device === "ONE"}
<GenericLayout bind:activeLayer />
<GenericLayout />
<!-- <LayoutCC1 bind:activeLayer /> -->
{:else}
<p>Unsupported device ({$serialPort?.device})</p>

View File

@@ -0,0 +1,19 @@
import type {CharaLayout} from "$lib/serialization/layout"
import type {Change} from "$lib/serial/connection"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
export function getActions(
layer: number,
id: number,
layout: CharaLayout,
changes: Change[],
): [KeyInfo, boolean] {
const actionId = layout?.[layer][id]
const changedId = changes.findLast(it => it?.layout?.[layer]?.[id] !== undefined)?.layout?.[layer]?.[id]
if (changedId !== undefined) {
return [KEYMAP_CODES[changedId] ?? {code: changedId}, true]
} else {
return [KEYMAP_CODES[actionId] ?? {code: actionId}, false]
}
}

View File

@@ -0,0 +1,9 @@
export interface VisualLayoutConfig {
scale: number
inactiveScale: number
inactiveOpacity: number
strokeWidth: number
margin: number
fontSize: number
iconFontSize: number
}

View File

@@ -1,17 +1,32 @@
export interface VisualLayout {
name: string
row: VisualLayoutRow[]
col: VisualLayoutRow[]
}
export interface VisualLayoutRow {
col: VisualLayoutKey[]
interface Positionable {
offset: [number, number]
rotate: number
}
export interface VisualLayoutKey {
id: number
export interface VisualLayoutRow extends Positionable {
row: Array<VisualLayoutKey | VisualLayoutSwitch>
}
export interface VisualLayoutKey extends Positionable {
key: number
size?: [number, number]
}
export interface VisualLayoutSwitch extends Positionable {
switch: {
n: number
e: number
w: number
s: number
d: number
}
}
export interface CompiledLayout {
name: string
size: [number, number]
@@ -20,9 +35,11 @@ export interface CompiledLayout {
export interface CompiledLayoutKey {
id: number
type: "key" | "dpad"
shape: "quarter-circle" | "square"
cornerRadius: number
size: [number, number]
pos: [number, number]
rotate: number
}
export function compileLayout(layout: VisualLayout): CompiledLayout {
@@ -33,21 +50,52 @@ export function compileLayout(layout: VisualLayout): CompiledLayout {
}
let y = 0
for (const {col} of layout.row) {
let x = 0
for (const {row, offset} of layout.col) {
let x = offset?.[0] ?? 0
y += offset?.[1] ?? 0
let maxHeight = 0
for (const {id, size} of col) {
const [width, height] = size ?? [1, 1]
for (const info of row) {
const [ox, oy] = info.offset || [0, 0]
const rotate = info.rotate || 0
if ("key" in info) {
const [width, height] = info.size ?? [1, 1]
compiled.keys.push({
id,
type: "key",
size: [width, height],
pos: [x, y],
})
compiled.keys.push({
id: info.key,
shape: "square",
size: [width, height],
pos: [x + ox, y + oy],
cornerRadius: 0.1,
rotate,
})
x += width
maxHeight = Math.max(maxHeight, height)
x += width + ox
maxHeight = Math.max(maxHeight, height + oy)
} else if ("switch" in info) {
const cx = x + ox + 1
const cy = y + oy + 1
for (const [id, i] of [info.switch.n, info.switch.e, info.switch.s, info.switch.w].entries()) {
compiled.keys.push({
id,
shape: "quarter-circle",
cornerRadius: 0,
size: [2, 0.6],
pos: [cx, cy],
rotate: (Math.PI / 2) * i + Math.PI / 4,
})
}
compiled.keys.push({
id: info.switch.d,
shape: "square",
cornerRadius: 0.5,
size: [0.8, 0.8],
pos: [x + 0.6 + ox, y + 0.6 + oy],
rotate: 0,
})
x += 2 + ox
maxHeight = Math.max(maxHeight, 2 + oy)
}
}
y += maxHeight
compiled.size[0] = Math.max(compiled.size[0], x)

View File

@@ -2,12 +2,14 @@
import {share} from "$lib/share"
import {layout} from "$lib/serial/connection"
import tippy from "tippy.js"
import {onMount} from "svelte"
import {onMount, setContext} from "svelte"
import Layout from "$lib/components/layout/Layout.svelte"
import {csvLayoutToJson, isCsvLayout} from "$lib/compat/legacy-layout"
import {charaFileFromUriComponent, charaFileToUriComponent} from "$lib/share/share-url"
import type {CharaLayoutFile} from "$lib/share/chara-file"
import SharePopup from "../SharePopup.svelte"
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
import {writable} from "svelte/store"
onMount(async () => {
const url = new URL(window.location.href)
@@ -53,6 +55,18 @@
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
}
setContext<VisualLayoutConfig>("visual-layout-config", {
scale: 50,
inactiveScale: 0.6,
inactiveOpacity: 0.4,
strokeWidth: 1,
margin: 5,
fontSize: 9,
iconFontSize: 14,
})
setContext("active-layer", writable(0))
let fileInput: HTMLInputElement
</script>