From c7f75cae5add3686eb742090fefab521bf60508e Mon Sep 17 00:00:00 2001 From: kchop Date: Mon, 13 Jan 2025 12:46:00 +0900 Subject: [PATCH] =?UTF-8?q?canvas=E3=82=92=E9=A0=98=E5=9F=9F=E5=86=85?= =?UTF-8?q?=E3=81=84=E3=81=A3=E3=81=B1=E3=81=84=E3=81=AB=E5=BA=83=E3=81=92?= =?UTF-8?q?=E3=81=A6=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=81=9F=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ひとまずリサイズはできるようになった * ちょっと反応が遅く感じるので250msに * いったん丸ごと再描画する方法で乗り切る * 完璧にリサイズできた * 初期値から可変にしておく * ここサイズ変えるのめんどくさいし中央に出すようにしよう * maxCanvasSizeで最大幅を制限する * maxCanvasSizeをUIで設定できるようにした --- package.json | 1 + pnpm-lock.yaml | 8 +++ src/camera/camera.ts | 84 +++++++++++++++++++++++- src/camera/palette.ts | 2 +- src/main.tsx | 7 ++ src/mandelbrot-state/mandelbrot-state.ts | 3 + src/p5-adapter/p5-adapter.ts | 10 +++ src/store/store.ts | 3 + src/store/sync-storage/settings.ts | 2 + src/view/canvas-overlay/index.tsx | 2 +- src/view/right-sidebar/settings.tsx | 28 ++++++++ 11 files changed, 145 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a109fe2..7980bb0 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "culori": "4.0.1", "d3-color": "3.1.0", "d3-scale-chromatic": "3.1.0", + "es-toolkit": "1.31.0", "eventmit": "2.0.4", "idb-keyval": "6.2.1", "lodash.throttle": "4.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85e4505..bff216e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: d3-scale-chromatic: specifier: 3.1.0 version: 3.1.0 + es-toolkit: + specifier: 1.31.0 + version: 1.31.0 eventmit: specifier: 2.0.4 version: 2.0.4 @@ -1710,6 +1713,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.31.0: + resolution: {integrity: sha512-vwS0lv/tzjM2/t4aZZRAgN9I9TP0MSkWuvt6By+hEXfG/uLs8yg2S1/ayRXH/x3pinbLgVJYT+eppueg3cM6tg==} + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -4604,6 +4610,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.31.0: {} + esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 diff --git a/src/camera/camera.ts b/src/camera/camera.ts index 388faf8..d375d05 100644 --- a/src/camera/camera.ts +++ b/src/camera/camera.ts @@ -1,10 +1,21 @@ import { getCurrentParams } from "@/mandelbrot-state/mandelbrot-state"; +import { getStore } from "@/store/store"; import type { IterationBuffer } from "@/types"; import p5 from "p5"; -import { getIterationCache } from "../iteration-buffer/iteration-buffer"; +import { + getIterationCache, + scaleIterationCacheAroundPoint, + setIterationCache, + translateRectInIterationCache, +} from "../iteration-buffer/iteration-buffer"; import { Rect } from "../math/rect"; import { renderIterationsToPixel } from "../rendering/rendering"; -import { getCurrentPalette, markAsRendered, needsRerender } from "./palette"; +import { + getCurrentPalette, + markAsRendered, + markNeedsRerender, + needsRerender, +} from "./palette"; let mainBuffer: p5.Graphics; @@ -16,7 +27,16 @@ let bufferRect: Rect; /** * FIXME: responsiveにするときに任意の値で初期化できるようにする */ -export const initializeCanvasSize = (w: number = 800, h: number = 800) => { +export const initializeCanvasSize = () => { + const elm = document.getElementById("canvas-wrapper"); + let w = 800; + let h = 800; + + if (elm) { + w = elm.clientWidth; + h = elm.clientHeight; + } + width = w; height = h; @@ -34,6 +54,64 @@ export const setupCamera = (p: p5, w: number, h: number) => { console.log("Camera setup done", { width, height }); }; +/** + * 画面サイズが変わったときに呼ぶ + * + * やること + * - canvasのリサイズ + * - mainBufferのリサイズ + * - cacheの位置変更(できれば) + */ +export const resizeCamera = ( + p: p5, + requestWidth: number, + requestHeight: number, +) => { + const from = getCanvasSize(); + console.debug( + `Request resize canvas to w=${requestWidth} h=${requestHeight}, from w=${from.width} h=${from.height}`, + ); + + const maxSize = getStore("maxCanvasSize"); + + const w = maxSize === -1 ? requestWidth : Math.min(requestWidth, maxSize); + const h = maxSize === -1 ? requestHeight : Math.min(requestHeight, maxSize); + + console.debug(`Resize to: w=${w}, h=${h} (maxCanvasSize=${maxSize})`); + + p.resizeCanvas(w, h); + + width = w; + height = h; + bufferRect = { x: 0, y: 0, width: w, height: h }; + + mainBuffer.resizeCanvas(width, height); + clearMainBuffer(); + + const scaleFactor = + Math.min(width, height) / Math.min(from.width, from.height); + + console.debug("Resize scale factor", scaleFactor); + + // サイズ差の分trasnlateしてからscale + + const offsetX = Math.round((width - from.width) / 2); + const offsetY = Math.round((height - from.height) / 2); + translateRectInIterationCache(-offsetX, -offsetY); + + const translated = scaleIterationCacheAroundPoint( + width / 2, + height / 2, + scaleFactor, + width, + height, + ); + setIterationCache(translated); + renderToMainBuffer(); + + markNeedsRerender(); +}; + export const nextBuffer = (_p: p5): p5.Graphics => { if (needsRerender()) { markAsRendered(); diff --git a/src/camera/palette.ts b/src/camera/palette.ts index f817f83..d4819a3 100644 --- a/src/camera/palette.ts +++ b/src/camera/palette.ts @@ -24,7 +24,7 @@ let renderNext = true; /** * 次に再renderするようマークする */ -const markNeedsRerender = () => { +export const markNeedsRerender = () => { renderNext = true; }; diff --git a/src/main.tsx b/src/main.tsx index 46b7521..3a010c2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,4 @@ +import { debounce } from "es-toolkit"; import p5 from "p5"; import React from "react"; import ReactDOMClient from "react-dom/client"; @@ -8,6 +9,7 @@ import { p5Draw, p5MouseReleased, p5Setup, + resizeTo, zoomTo, } from "./p5-adapter/p5-adapter"; import { isInside } from "./p5-adapter/utils"; @@ -71,6 +73,11 @@ const sketch = (p: p5) => { p.draw = () => { p5Draw(p); }; + + const debouncedResizeFunc = debounce(() => resizeTo(p), 250); + p.windowResized = () => { + debouncedResizeFunc(); + }; }; const entrypoint = () => { diff --git a/src/mandelbrot-state/mandelbrot-state.ts b/src/mandelbrot-state/mandelbrot-state.ts index a27288f..9968e71 100644 --- a/src/mandelbrot-state/mandelbrot-state.ts +++ b/src/mandelbrot-state/mandelbrot-state.ts @@ -52,6 +52,9 @@ export const markAsRenderedWithCurrentParams = () => { export const needsRenderForCurrentParams = () => { return !isSameParams(lastCalc, currentParams); }; +export const requireNextRender = () => { + lastCalc = { ...currentParams, N: 0 }; +}; const isSameParams = (a: MandelbrotParams, b: MandelbrotParams) => a.x === b.x && a.y === b.y && a.r === b.r && a.N === b.N && a.mode === b.mode; diff --git a/src/p5-adapter/p5-adapter.ts b/src/p5-adapter/p5-adapter.ts index f3eb396..70a6a56 100644 --- a/src/p5-adapter/p5-adapter.ts +++ b/src/p5-adapter/p5-adapter.ts @@ -2,6 +2,7 @@ import { getCanvasSize, initializeCanvasSize, nextBuffer, + resizeCamera, setupCamera, } from "@/camera/camera"; import { @@ -279,6 +280,15 @@ export const zoomTo = (isZoomOut: boolean) => { }); }; +/** wrapper elementの高さを取得してcameraのサイズを変える */ +export const resizeTo = (p: p5 = UNSAFE_p5Instance) => { + const elm = document.getElementById("canvas-wrapper"); + + if (elm) { + resizeCamera(p, elm.clientWidth, elm.clientHeight); + } +}; + // ================================================================================================ // 以下、p5.jsのcallback関数 // ================================================================================================ diff --git a/src/store/store.ts b/src/store/store.ts index 5414af7..5d61c4d 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -21,6 +21,8 @@ type Store = { workerCount: number; animationTime: number; refOrbitWorkerCount: number; + /** -1なら無制限。値があればcanvasの縦横幅はこの値以上にならない */ + maxCanvasSize: number; canvasLocked: boolean; shouldReuseRefOrbit: boolean; paletteLength: number; @@ -46,6 +48,7 @@ const store: Store = { workerCount: 2, animationTime: 0, refOrbitWorkerCount: 1, // 仮 + maxCanvasSize: -1, // UI state canvasLocked: false, // mandelbrot state diff --git a/src/store/sync-storage/settings.ts b/src/store/sync-storage/settings.ts index ac616ee..91a43c3 100644 --- a/src/store/sync-storage/settings.ts +++ b/src/store/sync-storage/settings.ts @@ -5,6 +5,7 @@ export type Settings = { workerCount: number; animationTime: number; animationCycleStep?: number; + maxCanvasSize?: number; }; export const DEFAULT_WORKER_COUNT = @@ -15,6 +16,7 @@ const defaultSettings = { workerCount: DEFAULT_WORKER_COUNT, animationTime: 0, animationCycleStep: 1, + maxCanvasSize: -1, } satisfies Settings; export const isSettingField = (key: string): key is keyof Settings => diff --git a/src/view/canvas-overlay/index.tsx b/src/view/canvas-overlay/index.tsx index b5436b0..a21198d 100644 --- a/src/view/canvas-overlay/index.tsx +++ b/src/view/canvas-overlay/index.tsx @@ -5,7 +5,7 @@ export const CanvasOverlay = () => { const progress = useStoreValue("progress"); return ( -
+
{typeof progress === "string" && }
); diff --git a/src/view/right-sidebar/settings.tsx b/src/view/right-sidebar/settings.tsx index c9425d5..35cd75c 100644 --- a/src/view/right-sidebar/settings.tsx +++ b/src/view/right-sidebar/settings.tsx @@ -1,4 +1,5 @@ import { ValueSlider } from "@/components/slider-wrapper"; +import { resizeTo } from "@/p5-adapter/p5-adapter"; import { Button } from "@/shadcn/components/ui/button"; import { useToast } from "@/shadcn/hooks/use-toast"; import { readPOIListFromClipboard } from "@/store/sync-storage/poi-list"; @@ -30,6 +31,7 @@ export const Settings = () => { const zoomRate = useStoreValue("zoomRate"); const workerCount = useStoreValue("workerCount"); + const maxCanvasSize = useStoreValue("maxCanvasSize"); const zoomRateValues = ["1.2", "1.5", "2.0", "4.0", "6.0", "10", "50", "100"]; const workerCountValues = createWorkerCountValues(); @@ -62,6 +64,15 @@ export const Settings = () => { "43", "47", ]; + const maxCanvasSizeValues = [ + "-1", + "128", + "256", + "512", + "800", + "1024", + "2048", + ]; const [zoomRatePreview, setZoomRatePreview] = useState(zoomRate); const [workerCountPreview, setWorkerCountPreview] = useState(workerCount); @@ -71,6 +82,8 @@ export const Settings = () => { const [animationCycleStep, setAnimationCycleStep] = useState(() => getStore("animationCycleStep"), ); + const [maxCanvasSizePreview, setMaxCanvasSizePreview] = + useState(maxCanvasSize); return (
@@ -127,6 +140,21 @@ export const Settings = () => { onValueCommit={(value) => updateStore("animationCycleStep", value)} />
+
+
Max Canvas Size: {maxCanvasSizePreview}
+ + values={maxCanvasSizeValues} + defaultValue={maxCanvasSize} + valueConverter={(value) => parseInt(value)} + onValueChange={(value) => { + setMaxCanvasSizePreview(value); + }} + onValueCommit={(value) => { + updateStore("maxCanvasSize", value); + resizeTo(); + }} + /> +
Import POI List