diff --git a/public/og/tr/linear-model-nedir.png b/public/og/tr/linear-model-nedir.png new file mode 100644 index 0000000..f5b9a5e Binary files /dev/null and b/public/og/tr/linear-model-nedir.png differ diff --git a/src/components/content/GradientVectorScene.astro b/src/components/content/GradientVectorScene.astro new file mode 100644 index 0000000..3681f3a --- /dev/null +++ b/src/components/content/GradientVectorScene.astro @@ -0,0 +1,325 @@ +--- +interface Vector3 { + x: number + y: number + z: number +} +interface PointInput { + position: Vector3 + color?: string + label?: string + size?: number +} +interface VectorInput { + origin?: Vector3 + direction: Vector3 + color?: string + length?: number + label?: string +} +interface LineInput { + from: Vector3 + to: Vector3 + color?: string + dashed?: boolean +} + +interface Props { + id?: string + height?: number + gradientDirection: Vector3 + gradientOrigin?: Vector3 + gradientColor?: string + gradientLength?: number + gradientLabel?: string + showPlane?: boolean + planeCenter?: Vector3 + planeNormal?: Vector3 + planeColor?: string + planeOpacity?: number + planeSize?: number + points?: PointInput[] + vectors?: VectorInput[] + lines?: LineInput[] + showGrid?: boolean + showAxes?: boolean + axisRange?: number + title?: string + xAxisLabel?: string + yAxisLabel?: string + zAxisLabel?: string +} + +const { + id = '', + height = 400, + gradientDirection, + gradientOrigin = { x: 0, y: 0, z: 0 }, + gradientColor = '#22c55e', + gradientLength, + gradientLabel = '∇f', + showPlane = false, + planeCenter = { x: 0, y: 0, z: 0 }, + planeNormal = { x: 0, y: 1, z: 0 }, + planeColor = '#71717a', + planeOpacity = 0.2, + planeSize, + points = [], + vectors = [], + lines = [], + showGrid = true, + showAxes = true, + axisRange = 5, + title = '', + xAxisLabel = 'X₁', + yAxisLabel = 'Y', + zAxisLabel = 'X₂', +} = Astro.props + +const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}` +--- + +
+
+
+ + + +
+
+ + diff --git a/src/components/content/HyperplaneScene.astro b/src/components/content/HyperplaneScene.astro new file mode 100644 index 0000000..c5e639e --- /dev/null +++ b/src/components/content/HyperplaneScene.astro @@ -0,0 +1,318 @@ +--- +interface Vector3 { + x: number + y: number + z: number +} +interface AxisRange { + min: number + max: number +} +interface PointInput { + position: Vector3 + color?: string + label?: string +} +interface LineInput { + from: Vector3 + to: Vector3 + color?: string + dashed?: boolean +} + +interface Props { + id?: string + height?: number + points?: PointInput[] + planeNormal: Vector3 + planePoint: Vector3 + planeColor?: string + planeOpacity?: number + showResiduals?: boolean + residualColor?: string + residualDashed?: boolean + lines?: LineInput[] + showGrid?: boolean + xRange?: AxisRange + yRange?: AxisRange + zRange?: AxisRange + title?: string + xAxisLabel?: string + yAxisLabel?: string + zAxisLabel?: string +} + +const { + id = '', + height = 400, + points = [], + planeNormal, + planePoint, + planeColor = '#3b82f6', + planeOpacity = 0.3, + showResiduals = true, + residualColor = '#ef4444', + residualDashed = true, + lines = [], + showGrid = true, + xRange, + yRange, + zRange, + title = '', + xAxisLabel = 'X₁', + yAxisLabel = 'Y', + zAxisLabel = 'X₂', +} = Astro.props + +const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}` +--- + +
+
+
+ + + +
+
+ + diff --git a/src/components/content/Scene3D.astro b/src/components/content/Scene3D.astro new file mode 100644 index 0000000..5f028aa --- /dev/null +++ b/src/components/content/Scene3D.astro @@ -0,0 +1,285 @@ +--- +interface Vector3 { + x: number + y: number + z: number +} +interface PointConfig { + position: Vector3 + color?: string + size?: number + label?: string + opacity?: number +} +interface VectorConfig { + origin?: Vector3 + direction: Vector3 + color?: string + length?: number + label?: string +} +interface LineConfig { + from: Vector3 + to: Vector3 + color?: string + dashed?: boolean +} +interface PlaneConfig { + center: Vector3 + normal: Vector3 + size?: number + color?: string + opacity?: number + label?: string +} + +interface Props { + id?: string + height?: number + points?: PointConfig[] + vectors?: VectorConfig[] + lines?: LineConfig[] + planes?: PlaneConfig[] + showGrid?: boolean + showAxes?: boolean + axisRange?: number + title?: string + xAxisLabel?: string + yAxisLabel?: string + zAxisLabel?: string +} + +const { + id = '', + height = 400, + points = [], + vectors = [], + lines = [], + planes = [], + showGrid = true, + showAxes = true, + axisRange = 5, + title = '', + xAxisLabel = 'X', + yAxisLabel = 'Y', + zAxisLabel = 'Z', +} = Astro.props + +const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}` +--- + +
+
+
+ + + +
+
+ + diff --git a/src/content/articles/tr/linear-model-nedir.mdx b/src/content/articles/tr/linear-model-nedir.mdx new file mode 100644 index 0000000..9b61ef1 --- /dev/null +++ b/src/content/articles/tr/linear-model-nedir.mdx @@ -0,0 +1,273 @@ +--- +lang: 'tr' +slug: 'linear-model-nedir' +title: 'Linear Model Nedir?' +excerpt: 'Lineer model, etiketlenmiş veri setlerindeki girdi ve çıktı değişkenleri arasındaki doğrusal ilişkiyi matematiksel olarak modellemek için kullanılan temel bir supervised (gözetimli) makine öğrenmesi algoritmasıdır.' +category: 'machine-learning' +tags: ['linear-regression', 'supervised-learning', 'teori'] +author: '@omerfdmrl' +date: '2026-03-29' +views: 0 +status: 'Published' +--- + +import HyperplaneScene from '../../../components/content/HyperplaneScene.astro' +import GradientVectorScene from '../../../components/content/GradientVectorScene.astro' + +Makine öğrenmesi ve istatistik dünyasına atılan ilk adım genellikle **Lineer Modeller** ile başlar. +Onlarca yıldır veri biliminin en temel yapı taşı olan bu yaklaşım, veriler arasındaki karmaşık gibi görünen ilişkileri basit ve doğrusal bir denklemle ifade etmemizi sağlar. +Özünde yaptığı iş çok nettir: Verilen input vektörü ile $X^T = (X_1, X_2, X_3 ...)$ output değeri $Y$'yi tahmin etmeye çalışır: + +$$ +\hat{Y} = \hat{\beta}_0 + \sum_{j=1}^p X_j \hat{\beta}_j +$$ + +$\hat{\beta}_0$ **bias (intercept)** değeridir. Bu formülü $X$'i vektör olarak yazarsak: + +$$ +\hat{Y} = X^{\top} \hat{\beta} +$$ + +olarak kullanabiliriz. + +## Dot Product Anlamak + +Bunu daha iyi anlamak için formülü tek tek açalım: + +$$ +\hat{Y}=\hat{\beta}_0 + X_1\hat{\beta}_1 + X_2\hat{\beta}_2 + \cdots + X_p\hat{\beta}_p +$$ + +Burada her $X_j$ tek bir feature değeridir ve tek sayıdır: + +$$ +X_j \in \mathbb{R} +$$ + +Aynı şekilde her katsayı da tek sayıdır: + +$$ +\hat{\beta}_j \in \mathbb{R} +$$ + +Dolayısıyla iki reel sayının çarpımı yine tek bir reel sayı verir: + +$$ +X_j \hat{\beta}_j \in \mathbb{R} +$$ + +Yani örneğin: + +$$ +X_1=3,\quad \hat{\beta}_1=4 +$$ + +ise: + +$$ +X_1\hat{\beta}_1 = 12 +$$ + +elde edilir. + +Bu 12 artık bir **skaler**dir; yani vektör veya matris değil, yalnızca tek bir sayıdır. + +Aynı durum tüm terimler için geçerlidir: + +$$ +X_2\hat{\beta}_2,\; X_3\hat{\beta}_3,\; \ldots,\; X_p\hat{\beta}_p +$$ + +hepsi tek tek skaler değerler üretir. + +Sonuçta bunların toplamı da yine tek bir sayı olur: + +$$ +\hat{Y}\in \mathbb{R} +$$ + +Bu nedenle lineer modelin çıktısı tek bir skaler tahmindir. +Dolayısıyla her çarpım da skaler olur, ve buna **dot product (inner product)** nedir. + +### Bias'ı Dahil Etmek + +Bias' ayrı yazmak yerine feature vektörünü genişletebiliriz: + +$$ +X = +\begin{bmatrix} +1 \\ X_1 \\ X_2 \\ \vdots \\ X_p +\end{bmatrix}, +\quad +\hat{\beta} = +\begin{bmatrix} +\hat{\beta}_0 \\ \hat{\beta}_1 \\ \hat{\beta}_2 \\ \vdots \\ \hat{\beta}_p +\end{bmatrix} +$$ + +Burada 1 ile başlatma sebebimiz, formüldeki $\hat{\beta}_0$ değerini de ekleyebilmek için. + +### Neden Transpose Gerekiyor? + +İki sütun vektörünü doğrudan çarpamayız. + +$$ +X = +\begin{bmatrix} +1 \\ X_1 \\ X_2 \\ \vdots \\ X_p +\end{bmatrix} += +(p + 1)\times1, +\quad +\hat{\beta} = +\begin{bmatrix} +\hat{\beta}_0 \\ \hat{\beta}_1 \\ \hat{\beta}_2 \\ \vdots \\ \hat{\beta}_p +\end{bmatrix} = +(p + 1)\times1 +$$ + +Dot product çarpımında içerikde boyutların aynı olması gerekir. Bu yüzden de birini satır olarak yazmamız gerekir. + +$$ +((p + 1)\times1) \times ((p + 1)\times1) \rightarrow (1\times(p + 1)) \times ((p + 1)\times1) +$$ + +$$ += 1\times1 +$$ + +Yani artık: + +$$ +X = +\begin{bmatrix} +1 & X_1 & X_2 & \ldots & X_p +\end{bmatrix} += +1\times(p + 1), +\quad +\hat{\beta} = +\begin{bmatrix} +\hat{\beta}_0 \\ \hat{\beta}_1 \\ \hat{\beta}_2 \\ \vdots \\ \hat{\beta}_p +\end{bmatrix} = +(p + 1)\times1 +$$ + +### Örnek Çözüm + +İki feature olsun: + +$$ +X= +\begin{bmatrix} +1\\ +3\\ +5 +\end{bmatrix} +,\quad +\hat{\beta}= +\begin{bmatrix} +2\\ +4\\ +6 +\end{bmatrix} +$$ + +O zaman: + +$$ +X^\top \hat{\beta} += +\begin{bmatrix} +1 & 3 & 5 +\end{bmatrix} +\begin{bmatrix} +2\\ +4\\ +6 +\end{bmatrix} +$$ + +Sonuç: + +$$ +=1\cdot2 + 3\cdot4 + 5\cdot6 +$$ + +$$ +=44 +$$ + +## Hiperdüzlem (Hyperplane) + +Lineer modelin geometrik anlamı bir **hiperdüzlem**dir. İki feature ($X_1, X_2$) ve bir output ($Y$) ile düşünelim. +Modelimizin ürettiği tahminler ($\hat{Y}$), $X_1$ ve $X_2$ eksenleri boyunca uzanan düz bir yüzey, yani bir düzlem (veya daha yüksek boyutlarda hiperdüzlem) oluşturur. + +Aşağıdaki görselleştirmede; mavi düzlem lineer modelimizin uzayda oluşturduğu hiperdüzlemi, noktalar gerçek veri noktalarını ve kırmızı kesikli çizgiler ise gerçek verilerin bizim modelimize (düzleme) olan uzaklıklarını gösterir: + + + +Her nokta, hiperdüzleme dikey olarak izdüşürülür. Gerçek değer ($Y$) ile modelin düzlem üzerindeki tahmini ($\hat{Y}$) arasındaki bu farklara **residual (artık)** denir: +$e_i = Y_i - \hat{Y}_i$. + +> **Not:** Bu düzlemin uzaya nasıl yerleştirileceği ve aradaki bu residual hatalarının nasıl en aza indirileceği, optimizasyon yöntemlerinin konusudur. Lineer model sadece bu düzlemin matematiksel tanımıdır. + +### Katsayılar ve Gradient Vektörü + +Lineer modelin denklemini bir fonksiyon olarak, $f(X) = X^\top \beta$ şeklinde düşündüğümüzde, modelin uzaydaki eğimini belirleyen şey $\beta$ katsayılarıdır. Matematiksel olarak bir fonksiyonun **gradyanı (gradient)**, o noktada fonksiyonun en dik artış yönünü gösterir. + +Lineer bir model için bu gradyan doğrudan katsayılar vektörüne eşittir: + +$$ +f'(X) = \nabla f(X) = \beta +$$ + +Yani $\beta$ vektörü, input uzayında modelimizin çıktısının ($\hat{Y}$) en hızlı arttığı yönü gösterir. + + + +Yukarıdaki görselleştirmede yeşil ok $\nabla f$ (yani $\beta$) vektörünü, turuncu nokta ise mevcut konum $X$'i temsil eder. +Lineer modelde düzlemin eğimi sabit olduğundan, bu yön her yerde aynıdır. Modelleri eğitirken kullandığımız algoritmalar (örneğin Gradient Descent), bu vektörel özellikleri kullanarak modeli en doğru konuma getirmeye çalışır. diff --git a/src/data/tr/articles.json b/src/data/tr/articles.json index 8743e99..0badc07 100644 --- a/src/data/tr/articles.json +++ b/src/data/tr/articles.json @@ -1,4 +1,9 @@ [ + { + "title": "Linear Model Nedir?", + "slug": "linear-model-nedir", + "category": "machine-learning" + }, { "title": "Linear Regression Nedir?", "slug": "linear-regression-nedir", diff --git a/src/utils/scene3d.ts b/src/utils/scene3d.ts new file mode 100644 index 0000000..3b931a6 --- /dev/null +++ b/src/utils/scene3d.ts @@ -0,0 +1,418 @@ +import * as THREE from 'three' +import { OrbitControls } from 'three/addons/controls/OrbitControls.js' + +export interface Vec3 { + x: number + y: number + z: number +} + +export interface ThemeColors { + textColor: number + gridColor: number + axisColor: number + bgCss: string + darkText: string + lightText: string +} + +export function getThemeColors3D(): ThemeColors { + const isDark = document.documentElement.classList.contains('dark') + return { + textColor: isDark ? 0xd4d4d8 : 0x52525b, + gridColor: isDark ? 0x3f3f46 : 0xdde1e7, + axisColor: isDark ? 0x71717a : 0x94a3b8, + bgCss: isDark ? '#0c0c0e' : '#f8fafc', + darkText: isDark ? '#e4e4e7' : '#27272a', + lightText: isDark ? '#a1a1aa' : '#71717a', + } +} + +export function makeLabel( + text: string, + position: THREE.Vector3, + colorHex: number, + scaleW = 0.28, + scaleH = 0.1, + fontPx = 28, +): THREE.Sprite { + const canvas = document.createElement('canvas') + canvas.width = 256 + canvas.height = 64 + const ctx = canvas.getContext('2d')! + ctx.clearRect(0, 0, 256, 64) + ctx.fillStyle = `#${colorHex.toString(16).padStart(6, '0')}` + ctx.font = `${fontPx}px monospace` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(text, 128, 32) + const texture = new THREE.CanvasTexture(canvas) + texture.needsUpdate = true + const mat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }) + const sprite = new THREE.Sprite(mat) + sprite.position.copy(position) + sprite.scale.set(scaleW, scaleH, 1) + return sprite +} + +export function createBaseScene(): THREE.Scene { + const scene = new THREE.Scene() + scene.add(new THREE.AmbientLight(0xffffff, 0.7)) + const dLight = new THREE.DirectionalLight(0xffffff, 0.9) + dLight.position.set(3, 5, 3) + scene.add(dLight) + const dLight2 = new THREE.DirectionalLight(0xffffff, 0.25) + dLight2.position.set(-2, -1, -2) + scene.add(dLight2) + return scene +} + +export function createCamera( + width: number, + height: number, + position: THREE.Vector3, + target: THREE.Vector3, +): THREE.PerspectiveCamera { + const camera = new THREE.PerspectiveCamera(40, width / height, 0.01, 200) + camera.position.copy(position) + camera.lookAt(target) + return camera +} + +export function createRenderer(container: HTMLElement, width: number, height: number): THREE.WebGLRenderer { + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }) + renderer.setSize(width, height) + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) + renderer.setClearColor(0x000000, 0) + renderer.domElement.style.cssText = 'position:absolute;top:0;left:0;z-index:1;border-radius:8px;' + container.style.position = 'relative' + container.appendChild(renderer.domElement) + return renderer +} + +export function createControls( + camera: THREE.PerspectiveCamera, + element: HTMLElement, + target: THREE.Vector3, + cubeSize: number, +): OrbitControls { + const controls = new OrbitControls(camera, element) + controls.enableDamping = true + controls.dampingFactor = 0.06 + controls.rotateSpeed = 0.7 + controls.zoomSpeed = 1.2 + controls.minDistance = cubeSize * 0.5 + controls.maxDistance = cubeSize * 8 + controls.target.copy(target) + controls.update() + return controls +} + +export function createAxisArrows( + cube: number, + theme: ThemeColors, + scene: THREE.Scene, + labels: { x: string; y: string; z: string }, + tickLabels: { x: string[]; y: string[]; z: string[] }, +): void { + const arrowLen = cube + 0.15 + const headLen = 0.08 + const headWid = 0.04 + + const xDir = new THREE.Vector3(1, 0, 0) + const yDir = new THREE.Vector3(0, 1, 0) + const zDir = new THREE.Vector3(0, 0, 1) + + scene.add(new THREE.ArrowHelper(xDir, new THREE.Vector3(0, 0, 0), arrowLen, theme.axisColor, headLen, headWid)) + scene.add(new THREE.ArrowHelper(yDir, new THREE.Vector3(0, 0, 0), arrowLen, theme.axisColor, headLen, headWid)) + scene.add(new THREE.ArrowHelper(zDir, new THREE.Vector3(0, 0, 0), arrowLen, theme.axisColor, headLen, headWid)) + + const tickOffset = cube * 0.02 + + scene.add( + makeLabel( + labels.x, + new THREE.Vector3(cube / 2, -tickOffset * 2.5, cube + tickOffset * 2), + theme.textColor, + 0.3, + 0.1, + 30, + ), + ) + scene.add( + makeLabel( + labels.y, + new THREE.Vector3(-tickOffset * 3, cube / 2, cube + tickOffset * 2), + theme.textColor, + 0.3, + 0.1, + 30, + ), + ) + scene.add( + makeLabel( + labels.z, + new THREE.Vector3(-tickOffset * 3, -tickOffset * 2.5, cube / 2), + theme.textColor, + 0.3, + 0.1, + 30, + ), + ) + + for (let i = 0; i < tickLabels.x.length; i++) { + const t = i / (tickLabels.x.length - 1) + const pos = t * cube + scene.add( + makeLabel( + tickLabels.x[i], + new THREE.Vector3(pos, -tickOffset, cube + tickOffset), + theme.textColor, + 0.2, + 0.08, + 24, + ), + ) + } + for (let i = 0; i < tickLabels.y.length; i++) { + const t = i / (tickLabels.y.length - 1) + const pos = t * cube + scene.add( + makeLabel( + tickLabels.y[i], + new THREE.Vector3(-tickOffset * 1.5, pos, cube + tickOffset), + theme.textColor, + 0.2, + 0.08, + 24, + ), + ) + } + for (let i = 0; i < tickLabels.z.length; i++) { + const t = i / (tickLabels.z.length - 1) + const pos = t * cube + scene.add( + makeLabel( + tickLabels.z[i], + new THREE.Vector3(-tickOffset * 1.5, -tickOffset, pos), + theme.textColor, + 0.2, + 0.08, + 24, + ), + ) + } +} + +export function createGridLines(cube: number, theme: ThemeColors, scene: THREE.Scene, divisions = 6): void { + function makeFaceGrid(origin: THREE.Vector3, uDir: THREE.Vector3, vDir: THREE.Vector3): void { + const points: THREE.Vector3[] = [] + for (let i = 0; i <= divisions; i++) { + const t = (i / divisions) * cube + points.push(origin.clone().addScaledVector(vDir, t)) + points.push(origin.clone().addScaledVector(uDir, cube).addScaledVector(vDir, t)) + points.push(origin.clone().addScaledVector(uDir, t)) + points.push(origin.clone().addScaledVector(uDir, t).addScaledVector(vDir, cube)) + } + const geo = new THREE.BufferGeometry().setFromPoints(points) + const mat = new THREE.LineBasicMaterial({ color: theme.gridColor, transparent: true, opacity: 0.35 }) + scene.add(new THREE.LineSegments(geo, mat)) + } + + makeFaceGrid(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)) + makeFaceGrid(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)) + makeFaceGrid(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0)) +} + +export function renderPoint( + scene: THREE.Scene, + position: THREE.Vector3, + color: number, + size = 8, + opacity = 1, +): THREE.Mesh { + const geo = new THREE.SphereGeometry(size * 0.005, 20, 20) + const mat = new THREE.MeshStandardMaterial({ + color, + transparent: opacity < 1, + opacity, + roughness: 0.35, + metalness: 0.15, + }) + const mesh = new THREE.Mesh(geo, mat) + mesh.position.copy(position) + scene.add(mesh) + return mesh +} + +export function renderVector( + scene: THREE.Scene, + origin: THREE.Vector3, + direction: THREE.Vector3, + color: number, + length?: number, +): THREE.ArrowHelper { + const len = length ?? direction.length() + const arrow = new THREE.ArrowHelper(direction.clone().normalize(), origin, len, color, len * 0.15, len * 0.06) + scene.add(arrow) + return arrow +} + +export function renderLine( + scene: THREE.Scene, + from: THREE.Vector3, + to: THREE.Vector3, + color: number, + dashed = false, +): THREE.Line | THREE.LineSegments { + if (dashed) { + const geo = new THREE.BufferGeometry().setFromPoints([from, to]) + const mat = new THREE.LineDashedMaterial({ + color, + dashSize: 0.05, + gapSize: 0.03, + transparent: true, + opacity: 0.8, + }) + const line = new THREE.Line(geo, mat) + line.computeLineDistances() + scene.add(line) + return line + } + const geo = new THREE.BufferGeometry().setFromPoints([from, to]) + const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.8 }) + const line = new THREE.Line(geo, mat) + scene.add(line) + return line +} + +export function renderPlane( + scene: THREE.Scene, + center: THREE.Vector3, + normal: THREE.Vector3, + size: number, + color: number, + opacity = 0.35, +): THREE.Mesh { + const geo = new THREE.PlaneGeometry(size, size) + const mat = new THREE.MeshStandardMaterial({ + color, + transparent: true, + opacity, + side: THREE.DoubleSide, + roughness: 0.6, + metalness: 0.1, + }) + const mesh = new THREE.Mesh(geo, mat) + mesh.position.copy(center) + + const up = new THREE.Vector3(0, 1, 0) + const norm = normal.clone().normalize() + if (Math.abs(norm.dot(up)) > 0.999) { + mesh.rotation.x = norm.y > 0 ? -Math.PI / 2 : Math.PI / 2 + } else { + mesh.lookAt(center.clone().add(norm)) + } + scene.add(mesh) + return mesh +} + +export function projectPointOnPlane( + point: THREE.Vector3, + planeNormal: THREE.Vector3, + planePoint: THREE.Vector3, +): THREE.Vector3 { + const n = planeNormal.clone().normalize() + const diff = point.clone().sub(planePoint) + const dist = diff.dot(n) + return point.clone().sub(n.clone().multiplyScalar(dist)) +} + +export function computeResidual( + point: THREE.Vector3, + planeNormal: THREE.Vector3, + planePoint: THREE.Vector3, +): { from: THREE.Vector3; to: THREE.Vector3 } { + return { + from: point.clone(), + to: projectPointOnPlane(point, planeNormal, planePoint), + } +} + +export function setupZoomControls( + container: HTMLElement, + camera: THREE.PerspectiveCamera, + controls: OrbitControls, + defaultEye: THREE.Vector3, + center: THREE.Vector3, +): void { + const duration = 300 + const zoomFactor = 0.75 + + container.querySelectorAll('.chart-btn').forEach((btn) => { + btn.addEventListener('click', () => { + if ((btn as HTMLElement).classList.contains('zoom-in')) { + const targetPos = camera.position + .clone() + .sub(controls.target) + .normalize() + .multiplyScalar(camera.position.distanceTo(controls.target) * zoomFactor) + .add(controls.target) + const startPos = camera.position.clone() + const zoomStart = performance.now() + function zoomInAnim() { + const p = Math.min((performance.now() - zoomStart) / duration, 1) + const e = 1 - Math.pow(1 - p, 3) + camera.position.lerpVectors(startPos, targetPos, e) + controls.update() + if (p < 1) requestAnimationFrame(zoomInAnim) + } + zoomInAnim() + } else if ((btn as HTMLElement).classList.contains('zoom-out')) { + const targetPos = camera.position + .clone() + .sub(controls.target) + .normalize() + .multiplyScalar(camera.position.distanceTo(controls.target) / zoomFactor) + .add(controls.target) + const startPos = camera.position.clone() + const zoomStart = performance.now() + function zoomOutAnim() { + const p = Math.min((performance.now() - zoomStart) / duration, 1) + const e = 1 - Math.pow(1 - p, 3) + camera.position.lerpVectors(startPos, targetPos, e) + controls.update() + if (p < 1) requestAnimationFrame(zoomOutAnim) + } + zoomOutAnim() + } else { + const resetStart = performance.now() + const fromPos = camera.position.clone() + const toPos = defaultEye.clone() + function resetAnim() { + const p = Math.min((performance.now() - resetStart) / 400, 1) + const e = 1 - Math.pow(1 - p, 3) + camera.position.lerpVectors(fromPos, toPos, e) + controls.target.copy(center) + controls.update() + if (p < 1) requestAnimationFrame(resetAnim) + } + resetAnim() + } + }) + }) +} + +export function createControlsHTML(): string { + return `
+ + + +
` +}