Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 96 additions & 60 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<div id="action-island">
<div class="action-btn-group">
<button id="resetActive" type="button" title="Reset">
<span class="material-symbols-outlined">restart_alt</span>
<span class="material-symbols-outlined">delete_outline</span>
<span>Reset</span>
</button>
<button id="undo" type="button" disabled title="Undo">
Expand All @@ -40,22 +40,15 @@
<span class="material-symbols-outlined">devices_fold</span>
<span>Fold</span>
</button>
<button id="flipPaper" type="button" title="Flip">
<span class="material-symbols-outlined">flip</span>
<span>Flip</span>
</button>
</div>
</div>

<!-- Settings & Info Buttons -->
<div id="settings-btn-container">
<button
id="toggleStyle"
type="button"
aria-controls="stylePanel"
aria-pressed="false"
aria-label="Style"
title="Style"
>
<span class="material-symbols-outlined">palette</span>
<span>Style</span>
</button>
<button
id="toggleSettings"
type="button"
Expand All @@ -81,48 +74,6 @@
</div>
</div>

<!-- Style Panel -->
<div id="stylePanel" class="settings-panel" style="display: none">
<h3>Style</h3>

<div class="settings-section">
<h4>Format</h4>
<div class="input-group">
<div class="paper-size-selector">
<div class="toggle-group">
<label
><input type="radio" name="paperSize" value="a4" checked /> A4</label
>
<label
><input type="radio" name="paperSize" value="square" /> Square</label
>
</div>
</div>
<label class="checkbox-label">
<input type="checkbox" id="showPaperBorder" checked />
Show paper border
</label>
</div>
</div>

<div class="settings-section">
<h4>Color</h4>
<div class="input-group">
<div class="input-row">
<label for="paperColor">Paper Color</label>
<div class="color-picker-wrapper">
<input id="paperColor" type="color" value="#ffffff" />
<div
class="color-picker-display"
id="paperColorDisplay"
style="background-color: #ffffff"
></div>
</div>
</div>
</div>
</div>
</div>

<!-- Settings Panel -->
<div id="settingsPanel" class="settings-panel" style="display: none">
<h3>Settings</h3>
Expand Down Expand Up @@ -154,7 +105,18 @@ <h4>Motion</h4>
</div>

<div class="settings-section">
<h4>Hinge</h4>
<div class="section-header">
<h4>Hinge</h4>
<button
id="resetHinge"
type="button"
class="reset-inline-btn"
title="Reset axis"
aria-label="Reset axis"
>
<span class="material-symbols-outlined icon-small">restart_alt</span>
</button>
</div>
<div class="input-group">
<div class="input-row mono">
<span class="hinge-label"
Expand Down Expand Up @@ -188,11 +150,80 @@ <h4>Hinge</h4>
<input id="manualHingeFlip" type="checkbox" />
Flip hinge line
</label>
<div class="mono hinge-status" id="hingeStatus" role="status"></div>
<button id="resetHinge" type="button" class="reset-hinge-btn">
<span class="material-symbols-outlined icon-small">restart_alt</span>
<span>Reset axis</span>
</button>
</div>
</div>

<div class="settings-section">
<h4>Style</h4>
<div class="input-group">
<div class="paper-size-selector">
<div class="toggle-group">
<label
><input type="radio" name="paperSize" value="a4" checked /> A4</label
>
<label
><input type="radio" name="paperSize" value="square" /> Square</label
>
<label
><input type="radio" name="paperSize" value="custom" /> Custom</label
>
</div>
<div
id="customAspectInputs"
class="custom-aspect-inputs"
style="display: none"
>
<div class="input-row mono">
<label for="customWidth">W:</label>
<input
id="customWidth"
type="number"
min="0.1"
max="100"
step="0.1"
value="3"
class="aspect-input"
/>
<span>:</span>
<label for="customHeight">H:</label>
<input
id="customHeight"
type="number"
min="0.1"
max="100"
step="0.1"
value="4"
class="aspect-input"
/>
</div>
</div>
</div>
<label class="checkbox-label">
<input type="checkbox" id="showPaperBorder" checked />
Show paper border
</label>
<div class="input-row">
<label for="paperFrontColor">Front color</label>
<div class="color-picker-wrapper">
<input id="paperFrontColor" type="color" value="#ffffff" />
<div
class="color-picker-display"
id="paperFrontColorDisplay"
style="background-color: #ffffff"
></div>
</div>
</div>
<div class="input-row">
<label for="paperBackColor">Back color</label>
<div class="color-picker-wrapper">
<input id="paperBackColor" type="color" value="#f0f0f0" />
<div
class="color-picker-display"
id="paperBackColorDisplay"
style="background-color: #f0f0f0"
></div>
</div>
</div>
</div>
</div>
</div>
Expand All @@ -207,6 +238,11 @@ <h3>Info</h3>
<div class="mono" id="status" role="status" aria-live="polite"></div>
</div>

<details class="debug-spoiler">
<summary>Debug</summary>
<div class="mono" id="debugStatus" role="status"></div>
</details>

<div class="settings-footer">
<a id="buyCoffee" href="https://www.buymeacoffee.com/maxwase" target="_blank">
<img
Expand Down
74 changes: 17 additions & 57 deletions src/device/hinge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,10 @@ export interface SegmentRect {
bottom: number;
width: number;
height: number;
x: number;
y: number;
}

export type SegmentSource =
| "viewportSegments"
| "visualViewportFunction"
| "visualViewportProperty"
| "windowSegments";

export interface HingeInfo {
hinge: Vec2;
segments: { source: SegmentSource; segments: SegmentRect[] };
segments: SegmentRect[];
hingeDir: Vec2;
}

Expand All @@ -29,7 +20,7 @@ interface WindowWithSegments extends Window {
getWindowSegments?: () => SegmentRect[] | null | undefined;
}

/** Resolve the hinge position and direction from viewport segments or fallback. */
/** Resolve the hinge direction and viewport segments (when available). */
export function computeHingePoint(canvasCssW: number, canvasCssH: number): HingeInfo {
const wAny = window as WindowWithSegments;

Expand All @@ -41,12 +32,7 @@ export function computeHingePoint(canvasCssW: number, canvasCssH: number): Hinge
}
const viewportSegments = readSegments(viewportSegmentsRaw);
if (viewportSegments.length > 0) {
return buildHingeInfo(
"viewportSegments",
viewportSegments,
canvasCssW,
canvasCssH,
);
return buildHingeInfo(viewportSegments, canvasCssW, canvasCssH);
}
}

Expand All @@ -62,12 +48,7 @@ export function computeHingePoint(canvasCssW: number, canvasCssH: number): Hinge
throw new Error("visualViewport.segments() did not return an array");
}
const funcSegments = readSegments(fromFunc);
return buildHingeInfo(
"visualViewportFunction",
funcSegments,
canvasCssW,
canvasCssH,
);
return buildHingeInfo(funcSegments, canvasCssW, canvasCssH);
}

if (
Expand All @@ -76,12 +57,7 @@ export function computeHingePoint(canvasCssW: number, canvasCssH: number): Hinge
) {
const valueSegments = readSegments(visualViewport.segments);
if (valueSegments.length > 0) {
return buildHingeInfo(
"visualViewportProperty",
valueSegments,
canvasCssW,
canvasCssH,
);
return buildHingeInfo(valueSegments, canvasCssW, canvasCssH);
}
}
}
Expand All @@ -93,37 +69,28 @@ export function computeHingePoint(canvasCssW: number, canvasCssH: number): Hinge
}
const windowSegments = readSegments(raw);
if (windowSegments.length > 0) {
return buildHingeInfo("windowSegments", windowSegments, canvasCssW, canvasCssH);
return buildHingeInfo(windowSegments, canvasCssW, canvasCssH);
}
}
} catch (err) {
// Fall through to fallback when segment APIs are missing or invalid.
console.warn(err);
}

const fallback = {
center: { x: canvasCssW / 2, y: canvasCssH / 2 },
width: canvasCssW,
height: canvasCssH,
};
return {
hinge: fallback.center,
segments: { source: "viewportSegments", segments: [] },
hingeDir: fallbackHingeDir(getScreenAngleDeg(), fallback.width, fallback.height),
segments: [],
hingeDir: fallbackHingeDir(getScreenAngleDeg(), canvasCssW, canvasCssH),
};
}

function buildHingeInfo(
source: SegmentSource,
segments: SegmentRect[],
canvasCssW: number,
canvasCssH: number,
): HingeInfo {
const { hinge, dir } = hingeFromSegments(segments, canvasCssW, canvasCssH);
return {
hinge,
segments: { source, segments },
hingeDir: dir,
segments,
hingeDir: hingeDirFromSegments(segments, canvasCssW, canvasCssH),
};
}

Expand All @@ -138,18 +105,13 @@ function readSegments(source: SegmentRect[]): SegmentRect[] {
}

/**
* Derive a hinge anchor and direction vector from provided screen segments.
* Falls back to the canvas center and orientation-derived direction
* when fewer than two segments are present.
* Derive a hinge direction vector from provided screen segments.
* Falls back to an orientation-derived direction when fewer than two segments exist.
*/
function hingeFromSegments(
segs: SegmentRect[],
w: number,
h: number,
): { hinge: Vec2; dir: Vec2 } {
function hingeDirFromSegments(segs: SegmentRect[], w: number, h: number): Vec2 {
const ratioDir = fallbackHingeDir(getScreenAngleDeg(), w, h);
if (segs.length < 2) {
return { hinge: { x: w / 2, y: h / 2 }, dir: ratioDir };
return ratioDir;
}

const byLeft = [...segs].sort((a, b) => a.left - b.left);
Expand All @@ -164,15 +126,13 @@ function hingeFromSegments(
const gapY = bT.top - aT.bottom;

if (gapX > 0 && gapX >= gapY) {
const hingeX = aL.right + gapX * 0.5;
return { hinge: { x: hingeX, y: h / 2 }, dir: { x: 0, y: 1 } };
return { x: 0, y: 1 };
}
if (gapY > 0 && gapY > gapX) {
const hingeY = aT.bottom + gapY * 0.5;
return { hinge: { x: w / 2, y: hingeY }, dir: { x: 1, y: 0 } };
return { x: 1, y: 0 };
}

return { hinge: { x: w / 2, y: h / 2 }, dir: ratioDir };
return ratioDir;
}

function hingeDirForAngle(angleDeg: number): Vec2 {
Expand Down
13 changes: 3 additions & 10 deletions src/device/motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export interface MotionTracker {
getAccel: () => Vec2;
/** Handler for devicemotion events. */
handleEvent: (e: DeviceMotionEvent) => void;
/** Update axis multiplier and gain at runtime. */
updateConfig: (config: MotionConfig) => void;
}

/**
Expand All @@ -27,9 +25,9 @@ export interface MotionTracker {
*/
export function createMotionTracker(config: MotionConfig = {}): MotionTracker {
let accel: Vec2 = { x: 0, y: 0 };
let axisMultiplier: Vec2 = config.axisMultiplier ?? { x: 1, y: 1 };
let gain = config.gain ?? 1;
let smoothing = config.smoothing ?? 0.25;
const axisMultiplier: Vec2 = config.axisMultiplier ?? { x: 1, y: 1 };
const gain = config.gain ?? 1;
const smoothing = config.smoothing ?? 0.25;

return {
getAccel: () => ({
Expand All @@ -46,10 +44,5 @@ export function createMotionTracker(config: MotionConfig = {}): MotionTracker {
accel = { x: accel.x, y: lerp(accel.y, acc.y, smoothing) };
}
},
updateConfig: (cfg) => {
if (cfg.axisMultiplier) axisMultiplier = cfg.axisMultiplier;
if (typeof cfg.gain === "number") gain = cfg.gain;
if (typeof cfg.smoothing === "number") smoothing = cfg.smoothing;
},
};
}
Loading