From e5477a5e0e23f3be019bd03e564b477f1ea2a844 Mon Sep 17 00:00:00 2001 From: "Chiaki.C" Date: Thu, 10 Jul 2025 16:02:09 +0900 Subject: [PATCH 1/6] feat: add pressure system --- .prettierignore | 1 + pages/fontdrawer.js | 189 ++++++++++++- pages/index.html | 28 +- pages/ja.html | 24 +- pages/pressure-drawing.js | 565 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 .prettierignore create mode 100644 pages/pressure-drawing.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..22e8364 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +/* \ No newline at end of file diff --git a/pages/fontdrawer.js b/pages/fontdrawer.js index 6e3ad12..c5e229b 100644 --- a/pages/fontdrawer.js +++ b/pages/fontdrawer.js @@ -253,11 +253,51 @@ $(document).ready(async function () { const $progressBar = $('#progress-bar'); const $progressText = $('#progress-text'); + // 初始化 PressureDrawing 實例 + const pressureDrawing = new PressureDrawing(); + let pressureDrawingEnabled = false; + let pressureDrawingSettings = { + thinning: 0.3, + smoothing: 0.7, + streamline: 0.7 + }; + + // 更新筆壓繪圖狀態 + async function updatePressureDrawingStatus() { + const enabled = await loadFromDB('pressureDrawingEnabled'); + const moduleInitialized = await pressureDrawing.initialize(); + + // 預設啟用筆壓繪圖(除非明確設定為 N) + pressureDrawingEnabled = (enabled !== 'N') && moduleInitialized; + + // 載入筆壓繪圖設定 + pressureDrawingSettings.thinning = parseFloat(await loadFromDB('pressureThinning') || 0.3); + pressureDrawingSettings.smoothing = parseFloat(await loadFromDB('pressureSmoothing') || 0.7); + pressureDrawingSettings.streamline = parseFloat(await loadFromDB('pressureStreamline') || 0.7); + } + // 初始化 IndexedDB - initDB().then(() => { + initDB().then(async () => { console.log('IndexedDB 起動完成'); initCanvas(canvas); // 初始化九宮格底圖 $listSelect.change(); // 觸發一次 change 事件以載入第一個列表 + + // 初始化筆壓繪圖狀態 + await updatePressureDrawingStatus(); + + // 如果是第一次使用,保存預設值 + if (await loadFromDB('pressureDrawingEnabled') === null) { + await saveToDB('pressureDrawingEnabled', 'Y'); + } + if (await loadFromDB('pressureThinning') === null) { + await saveToDB('pressureThinning', 0.3); + } + if (await loadFromDB('pressureSmoothing') === null) { + await saveToDB('pressureSmoothing', 0.7); + } + if (await loadFromDB('pressureStreamline') === null) { + await saveToDB('pressureStreamline', 0.7); + } }).catch((error) => { console.error('IndexedDB 起動失敗', error); }); @@ -298,6 +338,11 @@ $(document).ready(async function () { undoStack.length = 0; // 清空復原堆疊 ctx.clearRect(0, 0, canvas.width, canvas.height); loadCanvasData(nowGlyph); + + // 重置筆壓檢測狀態 + if (pressureDrawingEnabled) { + pressureDrawing.resetPressureDetection(); + } } // 儲存畫布的功能 @@ -361,13 +406,29 @@ $(document).ready(async function () { saveToDB('lineWidth', lineWidth); // 儲存筆寬到 Local Storage }); + // 儲存背景用於筆壓繪圖的即時預覽 + let backgroundImageData = null; + // 開始繪製 - $canvas.on('mousedown touchstart', function (event) { + $canvas.on('mousedown touchstart pointerdown', function (event) { isDrawing = true; undoStack.push(canvas.toDataURL()); // 儲存當前畫布狀態到 undoStack const { x, y } = getCanvasCoordinates(event); - ctx.beginPath(); - ctx.moveTo(x * ratio, y * ratio); + + if (pressureDrawingEnabled) { + // 使用筆壓繪圖系統 + const pressure = pressureDrawing.simulatePressure(event.originalEvent, 'start'); + pressureDrawing.startStroke(x * ratio, y * ratio, pressure); + // 儲存背景圖像用於即時預覽 + backgroundImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // 防止預設的觸控行為(如滾動) + event.preventDefault(); + } else { + // 使用傳統繪圖系統 + ctx.beginPath(); + ctx.moveTo(x * ratio, y * ratio); + } }); var eraseMode = false; @@ -384,24 +445,75 @@ $(document).ready(async function () { }); // 繪製中 - $canvas.on('mousemove touchmove', function (event) { + $canvas.on('mousemove touchmove pointermove', function (event) { if (!isDrawing) return; const { x, y } = getCanvasCoordinates(event); - ctx.globalCompositeOperation = eraseMode ? "destination-out" : "source-over"; // 如果是橡皮擦模式,則使用 destination-out,否則使用 source-over - // 毛筆模式:動態調整線條粗細 - ctx.lineWidth = lineWidth * 0.7 + Math.random() * lineWidth * 0.6; // 粗細隨機變化 - ctx.lineJoin = 'round'; // 線條連接處為圓角 - ctx.lineCap = 'round'; // 線條端點為圓角 - ctx.lineTo(x*ratio, y*ratio); - ctx.strokeStyle = 'black'; - ctx.stroke(); + if (pressureDrawingEnabled) { + // 使用筆壓繪圖系統:收集點並提供即時預覽 + const pressure = pressureDrawing.simulatePressure(event.originalEvent, 'move'); + pressureDrawing.addPoint(x * ratio, y * ratio, pressure); + + // 生成即時預覽筆跡 + const previewStroke = pressureDrawing.createPreviewStroke({ + size: lineWidth, + thinning: pressureDrawingSettings.thinning, + smoothing: pressureDrawingSettings.smoothing, + streamline: pressureDrawingSettings.streamline + }); + + if (previewStroke && backgroundImageData) { + // 恢復背景圖像 + ctx.putImageData(backgroundImageData, 0, 0); + + // 繪製預覽筆跡 + pressureDrawing.drawStrokeOnCanvas(ctx, previewStroke, eraseMode); + } + + // 防止預設的觸控行為 + event.preventDefault(); + } else { + // 使用傳統繪圖系統 + ctx.globalCompositeOperation = eraseMode ? "destination-out" : "source-over"; // 如果是橡皮擦模式,則使用 destination-out,否則使用 source-over + // 毛筆模式:動態調整線條粗細 + ctx.lineWidth = lineWidth * 0.7 + Math.random() * lineWidth * 0.6; // 粗細隨機變化 + ctx.lineJoin = 'round'; // 線條連接處為圓角 + ctx.lineCap = 'round'; // 線條端點為圓角 + ctx.lineTo(x*ratio, y*ratio); + ctx.strokeStyle = 'black'; + ctx.stroke(); + } }); // 停止繪製 - $canvas.on('mouseup mouseleave touchend', function () { + $canvas.on('mouseup mouseleave touchend pointerup pointerleave', function (event) { + if (!isDrawing) return; isDrawing = false; - ctx.closePath(); + + if (pressureDrawingEnabled) { + // 使用筆壓繪圖系統:生成最終筆跡並繪製 + const finalStroke = pressureDrawing.finishStroke({ + size: lineWidth, + thinning: pressureDrawingSettings.thinning, + smoothing: pressureDrawingSettings.smoothing, + streamline: pressureDrawingSettings.streamline + }); + + if (finalStroke && backgroundImageData) { + // 恢復背景圖像 + ctx.putImageData(backgroundImageData, 0, 0); + + // 繪製最終筆跡 + pressureDrawing.drawStrokeOnCanvas(ctx, finalStroke, eraseMode); + } + + // 清除背景圖像數據 + backgroundImageData = null; + } else { + // 使用傳統繪圖系統 + ctx.closePath(); + } + saveToLocalDB(); // 停止繪製時儲存畫布內容到 Local Storage }); @@ -621,6 +733,26 @@ $(document).ready(async function () { $('#scaleRateSlider').val(scale); $('#scaleRateValue').text(scale + '%'); + // 載入筆壓繪圖設定 + const pressureEnabledSetting = await loadFromDB('pressureDrawingEnabled'); + const pressureEnabled = pressureEnabledSetting !== 'N'; // 預設啟用(除非明確設定為 N) + $('#pressureDrawingEnabled').prop('checked', pressureEnabled); + + const thinning = await loadFromDB('pressureThinning') || 0.3; + $('#pressureThinningSlider').val(thinning); + $('#pressureThinningValue').text(thinning); + + const smoothing = await loadFromDB('pressureSmoothing') || 0.7; + $('#pressureSmoothingSlider').val(smoothing); + $('#pressureSmoothingValue').text(smoothing); + + const streamline = await loadFromDB('pressureStreamline') || 0.7; + $('#pressureStreamlineSlider').val(streamline); + $('#pressureStreamlineValue').text(streamline); + + // 控制筆壓設定區域的顯示/隱藏 + $('#pressureSettings').toggle(pressureEnabled); + $('#spanAllCount').text(Object.keys(glyphMap).length); $('#spanDoneCount').text(await countGlyphFromDB()); }); @@ -640,6 +772,33 @@ $(document).ready(async function () { initCanvas(canvas); }); + // 筆壓繪圖設定事件監聽器 + $('#pressureDrawingEnabled').on('change', function () { + const enabled = $(this).prop('checked'); + saveToDB('pressureDrawingEnabled', enabled ? 'Y' : 'N'); + $('#pressureSettings').toggle(enabled); + // 更新筆壓繪圖狀態 + updatePressureDrawingStatus(); + }); + + $('#pressureThinningSlider').on('input', function () { + var value = parseFloat($(this).val()); + $('#pressureThinningValue').text(value); + saveToDB('pressureThinning', value); + }); + + $('#pressureSmoothingSlider').on('input', function () { + var value = parseFloat($(this).val()); + $('#pressureSmoothingValue').text(value); + saveToDB('pressureSmoothing', value); + }); + + $('#pressureStreamlineSlider').on('input', function () { + var value = parseFloat($(this).val()); + $('#pressureStreamlineValue').text(value); + saveToDB('pressureStreamline', value); + }); + // 顯示字表畫面 $('#canvasListButton').on('click', async function () { $('#listup-container').show(); diff --git a/pages/index.html b/pages/index.html index 1b3009b..d073a2a 100644 --- a/pages/index.html +++ b/pages/index.html @@ -36,10 +36,10 @@ #nextButton { font-size: 2.5em; position: absolute; right: 0; top: 0; border: 0; background: transparent; box-shadow: none } #canvas-container { position: relative; width: 360px; height: 360px; border: 1px solid #888; background-color: #fff;} - canvas { position: absolute; top: 0; left: 0; width: 360px; height: 360px } + canvas { position: absolute; top: 0; left: 0; width: 360px; height: 360px; touch-action: none; } @media screen and ((max-height: 700px) or (max-width: 380px)) { #canvas-container { width: 320px; height: 320px } - canvas { width: 320px; height: 320px } + canvas { width: 320px; height: 320px; touch-action: none; } } /* 九宮格底圖樣式 */ @@ -69,7 +69,7 @@ .dialog { display: none; position: fixed; top: 0; left: 0; width: 90%; height: 100%; background-color: rgba(0, 0, 0, 0.8); color: #fff; z-index: 999; text-align: left; padding: 5% } .dialog h2 { margin-top: 0.2em } .close { position: absolute; display: block; right: 20px; top: 15px; font: normal 3em/1 sans-serif} - .dialog-body { height: 84%; overflow: scroll } + .dialog-body { height: 100%; overflow: scroll } #listup-body img { display: block; background-color: #fff; width: 50px; height: 50px; float: left; border: 1px solid #ccc; margin: 5px } #listup-body span { display: block; background-color: #666; color: #ddd; width: 50px; height: 50px; float: left; border: 1px solid #ccc; line-height: 50px; text-align: center; font-size: 2em; margin: 5px; font-family: GenYoExt, LessonOne, sans-serif; overflow: hidden; } @@ -177,6 +177,27 @@

設定

設定為不等寬會更像手寫字型,但可能不適合用於直排。

+ + + 啟用後可提供更自然的筆壓效果,適合觸控筆或支援筆壓的設備。不支援的設備也會自動模擬筆壓繪圖。 +
+
+ + + 0.3 + 數值越大,壓力變化對線條粗細的影響越明顯。 +
+ + + 0.7 + 數值越大,線條越平滑。 +
+ + + 0.7 + 數值越大,線條越流暢,但筆壓變化可能較不明顯。 +
+


@@ -210,6 +231,7 @@

設定

clearDone: '已清除。' }; + \ No newline at end of file diff --git a/pages/ja.html b/pages/ja.html index 8abf6bc..2e2972e 100644 --- a/pages/ja.html +++ b/pages/ja.html @@ -68,7 +68,7 @@ .dialog { display: none; position: fixed; top: 0; left: 0; width: 90%; height: 100%; background-color: rgba(0, 0, 0, 0.8); color: #fff; z-index: 999; text-align: left; padding: 5% } .dialog h2 { margin-top: 0.2em } .close { position: absolute; display: block; right: 20px; top: 15px; font: normal 3em/1 sans-serif} - .dialog-body { height: 84%; overflow: scroll } + .dialog-body { height: 100%; overflow: scroll } #listup-body img { display: block; background-color: #fff; width: 50px; height: 50px; float: left; border: 1px solid #ccc; margin: 5px } #listup-body span { display: block; background-color: #666; color: #ddd; width: 50px; height: 50px; float: left; border: 1px solid #ccc; line-height: 50px; text-align: center; font-size: 2em; margin: 5px; font-family: LessonOne, sans-serif; overflow: hidden; } @@ -176,6 +176,27 @@

設定

手書きフォントの性格上、固定幅よりもプロポーショナル幅の方が自然に見える場合がありますが、縦書きとしての利用に不向きです。

+ + + 有効にすると、タッチペンや筆圧対応デバイスでより自然な筆圧効果を得られます。筆圧不対応の設備でも自動的に筆圧繪圖を模擬します。 +
+
+ + + 0.3 + 値が大きいほど、筆圧変化が線の太さに与える影響が大きくなります。 +
+ + + 0.7 + 値が大きいほど、線がより滑らかになります。 +
+ + + 0.7 + 値が大きいほど、線がより流暢になりますが、筆圧変化が目立ちにくくなる場合があります。 +
+


@@ -208,6 +229,7 @@

設定

clearDone: '削除しました' }; + diff --git a/pages/pressure-drawing.js b/pages/pressure-drawing.js new file mode 100644 index 0000000..4e1f0c2 --- /dev/null +++ b/pages/pressure-drawing.js @@ -0,0 +1,565 @@ +/** + * Pressure Drawing Module + * Uses perfect-freehand library for pressure-sensitive drawing + */ + +class PressureDrawing { + constructor() { + this.perfectFreehandModule = null; + this.currentStroke = []; + this.isDrawing = false; + this.lastPoint = null; + this.hasPressureSupport = false; // 檢測是否支援筆壓 + this.pressureCheckCount = 0; // 用於檢測筆壓支援 + this.delayedStart = false; // 是否在延遲繪製狀態 + this.startPoint = null; // 起筆點 + this.moveThreshold = 8; // 移動閾值(像素) + } + + // Initialize the perfect-freehand module + async initialize() { + try { + this.perfectFreehandModule = await import('https://unpkg.com/perfect-freehand@1.2.2/dist/esm/index.mjs'); + return true; + } catch (error) { + return false; + } + } + + // Start a new stroke + startStroke(x, y, pressure = 0.5) { + this.isDrawing = true; + this.currentStroke = []; + this.lastPoint = { x, y, pressure }; + this.delayedStart = true; // 進入延遲繪製狀態 + this.startPoint = { x, y, pressure }; + this.currentStroke.push([x, y, pressure]); + } + + // Add a point to the current stroke + addPoint(x, y, pressure = 0.5) { + if (!this.isDrawing) return; + + // 檢查是否在延遲繪製狀態 + if (this.delayedStart && this.startPoint) { + const dx = x - this.startPoint.x; + const dy = y - this.startPoint.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < this.moveThreshold) { + // 還沒有足夠的移動,只更新起始點的壓力 + this.startPoint.pressure = Math.max(this.startPoint.pressure, pressure); + this.currentStroke[0] = [this.startPoint.x, this.startPoint.y, this.startPoint.pressure]; + this.lastPoint = { x, y, pressure }; + return; + } else { + // 開始真正的繪製 + this.delayedStart = false; + } + } + + let isSimulatedPressure = false; + + // 如果沒有真實壓力支持且檢測計數足夠,才進行速度模擬 + if (pressure === 0.5 && this.lastPoint && !this.hasPressureSupport && this.pressureCheckCount > 3) { + const dx = x - this.lastPoint.x; + const dy = y - this.lastPoint.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Adjust pressure based on drawing speed (slower = more pressure) + const speedFactor = Math.min(1, 10 / Math.max(distance, 1)); + pressure = 0.4 + speedFactor * 0.4; // Range from 0.4 to 0.8,範圍較小避免極端值 + isSimulatedPressure = true; + } + + // 只對模擬的壓力值增強對比,讓效果更明顯 + if (isSimulatedPressure) { + if (pressure < 0.5) { + pressure = pressure * 0.9; // 低壓力稍微降低 + } else { + pressure = 0.4 + (pressure - 0.5) * 1.1; // 高壓力稍微增加 + } + } + + this.currentStroke.push([x, y, pressure]); + this.lastPoint = { x, y, pressure }; + } + + // Finish the current stroke and return the path + finishStroke(options = {}) { + if (!this.isDrawing || this.currentStroke.length < 2) { + this.isDrawing = false; + this.delayedStart = false; + this.startPoint = null; + return null; + } + + // 如果還在延遲繪製狀態,說明沒有足夠的移動,直接生成圓形點 + if (this.delayedStart && this.startPoint) { + this.isDrawing = false; + this.delayedStart = false; + const strokePoints = [[this.startPoint.x, this.startPoint.y, this.startPoint.pressure]]; + this.startPoint = null; + return this.generateCircularDot(strokePoints, options); + } + + // 動態決定是否模擬壓力 + const shouldSimulatePressure = !this.hasPressureSupport && this.pressureCheckCount > 3; + + const defaultOptions = { + size: 12, + thinning: 0.8, // 增加壓力對粗細的影響 + smoothing: 0.5, + streamline: 0.3, // 減少流線化,讓壓力變化更明顯 + simulatePressure: shouldSimulatePressure, // 動態決定是否模擬壓力 + easing: (t) => t, + start: { + taper: 0, + easing: (t) => t, + cap: true + }, + end: { + taper: 25, // 大幅增加結尾的漸減效果 + easing: (t) => Math.sin((t * Math.PI) / 2), // 使用正弦緩動,更平滑 + cap: true // 使用圓頭來避免尖銳結尾 + } + }; + + const finalOptions = { ...defaultOptions, ...options }; + + try { + // 複製 stroke 點,避免修改原始資料 + let strokePoints = [...this.currentStroke]; + + // 壓力平滑處理 + if (strokePoints.length > 5) { + strokePoints = this.smoothPressureValues(strokePoints); + } + + + + // 檢查是否為靜止點或極短筆跡 + if (strokePoints.length <= 3) { + this.isDrawing = false; + this.currentStroke = []; + this.delayedStart = false; + this.startPoint = null; + return this.generateCircularDot(strokePoints, finalOptions); + } + + // 檢查筆跡是否在很小的範圍內 + const bounds = this.calculateBounds(strokePoints); + const maxDistance = Math.max(bounds.width, bounds.height); + + if (maxDistance < 8) { // 如果筆跡範圍小於 8 像素 + this.isDrawing = false; + this.currentStroke = []; + this.delayedStart = false; + this.startPoint = null; + return this.generateCircularDot(strokePoints, finalOptions); + } + + // Get stroke outline from perfect-freehand + const outlinePoints = this.perfectFreehandModule.getStroke(strokePoints, finalOptions); + + this.isDrawing = false; + this.currentStroke = []; + this.delayedStart = false; + this.startPoint = null; + + return outlinePoints; + } catch (error) { + this.isDrawing = false; + this.currentStroke = []; + this.delayedStart = false; + this.startPoint = null; + return null; + } + } + + // Smooth pressure values to create natural light-heavy-light curve + smoothPressureValues(strokePoints) { + if (!strokePoints || strokePoints.length < 5) return strokePoints; + + const points = [...strokePoints]; + const len = points.length; + + // Step 1: 移除開始和結尾的不穩定區域 + let startIndex = 0; + let endIndex = len; + + // 找到壓力開始穩定的位置 + for (let i = 2; i < Math.min(len - 2, 15); i++) { + const pressureVariance = this.calculatePressureVariance(points, i - 2, i + 2); + if (pressureVariance < 0.15) { // 放寬穩定標準 + startIndex = i; + break; + } + } + + // 找到壓力結束穩定的位置 + for (let i = len - 3; i >= Math.max(startIndex + 2, len - 15); i--) { + const pressureVariance = this.calculatePressureVariance(points, i - 2, i + 2); + if (pressureVariance < 0.15) { // 放寬穩定標準 + endIndex = i + 1; + break; + } + } + + // Step 2: 截取穩定區域 + const stablePoints = points.slice(startIndex, endIndex); + + if (stablePoints.length < 3) return points; // 如果穩定區域太小,返回原始數據 + + // Step 3: 對穩定區域進行移動平均平滑 + const smoothedPoints = this.applyMovingAverage(stablePoints); + + // Step 4: 創建自然的起筆和收筆 + const naturalCurve = this.createNaturalPressureCurve(smoothedPoints); + + return naturalCurve; + } + + // Calculate pressure variance in a range + calculatePressureVariance(points, start, end) { + if (start < 0 || end >= points.length || end - start < 2) return 999; + + const pressures = points.slice(start, end + 1).map(p => p[2]); + const mean = pressures.reduce((a, b) => a + b) / pressures.length; + const variance = pressures.reduce((acc, p) => acc + Math.pow(p - mean, 2), 0) / pressures.length; + + return Math.sqrt(variance); + } + + // Apply moving average to smooth pressure values + applyMovingAverage(points) { + if (points.length < 3) return points; + + const smoothed = []; + const windowSize = 5; // 增加窗口大小,讓平滑效果更明顯 + + for (let i = 0; i < points.length; i++) { + const start = Math.max(0, i - Math.floor(windowSize / 2)); + const end = Math.min(points.length - 1, i + Math.floor(windowSize / 2)); + + let sumPressure = 0; + let count = 0; + + for (let j = start; j <= end; j++) { + sumPressure += points[j][2]; + count++; + } + + const avgPressure = sumPressure / count; + smoothed.push([points[i][0], points[i][1], avgPressure]); + } + + return smoothed; + } + + // Create natural pressure curve with light start and end + createNaturalPressureCurve(points) { + if (points.length < 3) return points; + + const result = [...points]; + const len = result.length; + + // 找到壓力的峰值位置 + let maxPressure = 0; + let maxIndex = Math.floor(len / 2); + + for (let i = 0; i < len; i++) { + if (result[i][2] > maxPressure) { + maxPressure = result[i][2]; + maxIndex = i; + } + } + + // 創建自然的壓力曲線:輕 -> 重 -> 輕 + for (let i = 0; i < len; i++) { + let factor = 1.0; + + if (i < maxIndex) { + // 起筆段:從 0.3 漸增到 1.0 + factor = 0.3 + 0.7 * (i / maxIndex); + } else { + // 收筆段:從 1.0 漸減到 0.2 + factor = 1.0 - 0.8 * ((i - maxIndex) / (len - 1 - maxIndex)); + factor = Math.max(0.2, factor); + } + + // 應用漸變係數,但保持原始壓力的相對變化 + result[i][2] = result[i][2] * factor; + } + + return result; + } + + // Calculate bounds of stroke points + calculateBounds(strokePoints) { + if (!strokePoints || strokePoints.length === 0) { + return { width: 0, height: 0, minX: 0, maxX: 0, minY: 0, maxY: 0 }; + } + + let minX = strokePoints[0][0]; + let maxX = strokePoints[0][0]; + let minY = strokePoints[0][1]; + let maxY = strokePoints[0][1]; + + for (let i = 1; i < strokePoints.length; i++) { + const [x, y] = strokePoints[i]; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + + return { + width: maxX - minX, + height: maxY - minY, + minX, maxX, minY, maxY + }; + } + + // Generate circular dot based on pressure + generateCircularDot(strokePoints, options) { + if (!strokePoints || strokePoints.length === 0) return []; + + // 計算中心點和平均壓力 + let centerX = 0; + let centerY = 0; + let totalPressure = 0; + + for (const point of strokePoints) { + centerX += point[0]; + centerY += point[1]; + totalPressure += point[2]; + } + + centerX /= strokePoints.length; + centerY /= strokePoints.length; + const avgPressure = totalPressure / strokePoints.length; + + // 根據壓力計算半徑 + const baseRadius = (options.size || 12) * 0.5; + const radius = baseRadius * (0.3 + avgPressure * 0.7); // 根據壓力調整大小 + + // 生成圓形的點 + const circlePoints = []; + const segments = 16; // 圓形分段數 + + for (let i = 0; i < segments; i++) { + const angle = (i * 2 * Math.PI) / segments; + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + circlePoints.push([x, y]); + } + + return circlePoints; + } + + // Convert stroke outline to SVG path + outlineToSVGPath(outlinePoints) { + if (!outlinePoints || outlinePoints.length < 2) return ''; + + const path = outlinePoints.reduce((acc, point, index) => { + const [x, y] = point; + if (index === 0) { + return `M${x},${y}`; + } + return `${acc}L${x},${y}`; + }, ''); + + return `${path}Z`; + } + + // Draw stroke outline on canvas + drawStrokeOnCanvas(ctx, outlinePoints, eraseMode = false) { + if (!outlinePoints || outlinePoints.length < 2) return; + + ctx.save(); + + // Set composite operation for erasing + ctx.globalCompositeOperation = eraseMode ? "destination-out" : "source-over"; + + // Create path from outline points + ctx.beginPath(); + outlinePoints.forEach((point, index) => { + const [x, y] = point; + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.closePath(); + + // Fill the path + ctx.fillStyle = eraseMode ? 'rgba(0,0,0,1)' : 'black'; + ctx.fill(); + + ctx.restore(); + } + + // Get current stroke points (for preview) + getCurrentStrokePoints() { + return [...this.currentStroke]; + } + + // Check if currently drawing + getIsDrawing() { + return this.isDrawing; + } + + // Cancel current stroke + cancelStroke() { + this.isDrawing = false; + this.currentStroke = []; + this.lastPoint = null; + this.delayedStart = false; + this.startPoint = null; + } + + // Reset pressure detection (useful when switching characters) + resetPressureDetection() { + this.hasPressureSupport = false; + this.pressureCheckCount = 0; + this.delayedStart = false; + this.startPoint = null; + } + + // Create a preview stroke (for real-time drawing feedback) + createPreviewStroke(options = {}) { + if (!this.isDrawing || this.currentStroke.length < 8) return null; + + // 在延遲繪製狀態下不生成預覽筆跡 + if (this.delayedStart) return null; + + // 動態決定是否模擬壓力 + const shouldSimulatePressure = !this.hasPressureSupport && this.pressureCheckCount > 3; + + const defaultOptions = { + size: 12, + thinning: 0.8, // 增加壓力對粗細的影響 + smoothing: 0.5, + streamline: 0.3, // 減少流線化,讓壓力變化更明顯 + simulatePressure: shouldSimulatePressure, // 動態決定是否模擬壓力 + easing: (t) => t, + start: { + taper: 0, + easing: (t) => t, + cap: true + }, + end: { + taper: 25, // 大幅增加結尾的漸減效果 + easing: (t) => Math.sin((t * Math.PI) / 2), // 使用正弦緩動,更平滑 + cap: true // 使用圓頭來避免尖銳結尾 + } + }; + + const finalOptions = { ...defaultOptions, ...options }; + + try { + // 對預覽筆跡也應用壓力平滑 + let previewPoints = [...this.currentStroke]; + if (previewPoints.length > 5) { + previewPoints = this.smoothPressureValues(previewPoints); + } + + return this.perfectFreehandModule.getStroke(previewPoints, finalOptions); + } catch (error) { + return null; + } + } + + // Simulate pressure from pointer events + simulatePressure(event, eventType = 'move') { + + + // 提筆事件特殊處理 - 使用較低的壓力值 + if (eventType === 'end' && this.lastPoint) { + return Math.max(0.05, this.lastPoint.pressure * 0.3); // 提筆時壓力大幅減少 + } + + // Try to get pressure from pointer event (works with Apple Pencil) + if (event && event.pressure !== undefined && event.pressure > 0.1 && event.pointerType === 'pen') { + // 只有筆類型的 pointer event 且壓力值 > 0.1 才算真實筆壓支援 + this.hasPressureSupport = true; + + // 提筆事件時限制最大壓力值 + let pressure = event.pressure; + if (eventType === 'end') { + pressure = Math.min(pressure, 0.6); // 提筆時限制最大壓力 + } + + return Math.max(0.1, Math.min(1.0, pressure)); + } + + // 非筆類型的 pointer events(如手指)使用模擬壓力 + if (event && event.pointerType && event.pointerType !== 'pen') { + this.pressureCheckCount++; + return 0.5; // 返回預設值,讓速度模擬邏輯處理 + } + + // Try to get pressure from touch event + if (event && event.touches && event.touches.length > 0) { + const touch = event.touches[0]; + + // Apple Pencil support through force property + if (touch.force !== undefined && touch.force > 0.1 && touch.touchType === 'stylus') { + // 只有觸控筆類型且 force > 0.1 才算真實筆壓支援 + this.hasPressureSupport = true; + + // 提筆事件時限制最大壓力值 + let force = touch.force; + if (eventType === 'end') { + force = Math.min(force, 0.6); // 提筆時限制最大壓力 + } + + return Math.max(0.1, Math.min(1.0, force)); + } + + // 其他觸控事件(手指觸控或沒有 touchType)不提供真實壓力,使用模擬壓力 + if (!touch.touchType || touch.touchType !== 'stylus') { + this.pressureCheckCount++; + return 0.5; // 返回預設值,讓速度模擬邏輯處理 + } + } + + // Try to get pressure from mouse/pointer events + if (event && event.buttons !== undefined && event.type && event.type.includes('mouse')) { + // For mouse events, don't claim pressure support and use default simulation + // 滑鼠事件不提供真實壓力,應該使用模擬壓力 + this.pressureCheckCount++; + return 0.5; // 返回預設值,讓 addPoint 中的速度模擬邏輯處理 + } + + // Check for webkitForce (Safari specific for Force Touch) + if (event && event.webkitForce !== undefined && event.webkitForce > 1.0) { + // 只有 webkitForce > 1.0 才算真實壓力支援(正常值為 1.0,有壓力時會超過) + this.hasPressureSupport = true; + + // 提筆事件時限制最大壓力值 + let force = event.webkitForce; + if (eventType === 'end') { + force = Math.min(force, 2.0); // 提筆時限制最大壓力 + } + + // webkitForce 的範圍通常是 1.0-3.0,需要映射到 0.1-1.0 + const normalizedForce = Math.max(0.1, Math.min(1.0, (force - 1.0) / 2.0 + 0.5)); + return normalizedForce; + } + + // 增加檢測計數 + this.pressureCheckCount++; + + // Default pressure simulation with slight randomization + if (eventType === 'end') { + return 0.3; // 提筆時使用固定的低壓力值 + } + return 0.5 + Math.random() * 0.3; // Random pressure between 0.5 and 0.8 + } +} + +// Export for use in other modules +window.PressureDrawing = PressureDrawing; \ No newline at end of file From 222e715e3c7fc769a8b2a62f296ac425316e99cb Mon Sep 17 00:00:00 2001 From: "Chiaki.C" Date: Thu, 10 Jul 2025 16:04:08 +0900 Subject: [PATCH 2/6] fix: update pressure drawing settings to reflect changes immediately --- pages/fontdrawer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pages/fontdrawer.js b/pages/fontdrawer.js index c5e229b..4b00b1c 100644 --- a/pages/fontdrawer.js +++ b/pages/fontdrawer.js @@ -773,30 +773,36 @@ $(document).ready(async function () { }); // 筆壓繪圖設定事件監聽器 - $('#pressureDrawingEnabled').on('change', function () { + $('#pressureDrawingEnabled').on('change', async function () { const enabled = $(this).prop('checked'); saveToDB('pressureDrawingEnabled', enabled ? 'Y' : 'N'); $('#pressureSettings').toggle(enabled); - // 更新筆壓繪圖狀態 - updatePressureDrawingStatus(); + // 立即更新筆壓繪圖狀態 + await updatePressureDrawingStatus(); }); $('#pressureThinningSlider').on('input', function () { var value = parseFloat($(this).val()); $('#pressureThinningValue').text(value); saveToDB('pressureThinning', value); + // 立即更新設定 + pressureDrawingSettings.thinning = value; }); $('#pressureSmoothingSlider').on('input', function () { var value = parseFloat($(this).val()); $('#pressureSmoothingValue').text(value); saveToDB('pressureSmoothing', value); + // 立即更新設定 + pressureDrawingSettings.smoothing = value; }); $('#pressureStreamlineSlider').on('input', function () { var value = parseFloat($(this).val()); $('#pressureStreamlineValue').text(value); saveToDB('pressureStreamline', value); + // 立即更新設定 + pressureDrawingSettings.streamline = value; }); // 顯示字表畫面 From 57e0f54e5a883990ac11910ae93730c2a63c6c96 Mon Sep 17 00:00:00 2001 From: "Chiaki.C" Date: Thu, 10 Jul 2025 16:29:29 +0900 Subject: [PATCH 3/6] fix: adjust smoothing settings in pressure drawing for better performance --- pages/fontdrawer.js | 16 +++++++++------- pages/index.html | 4 ++-- pages/ja.html | 4 ++-- pages/pressure-drawing.js | 16 ++++++++++++---- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/pages/fontdrawer.js b/pages/fontdrawer.js index 4b00b1c..86596a6 100644 --- a/pages/fontdrawer.js +++ b/pages/fontdrawer.js @@ -258,7 +258,7 @@ $(document).ready(async function () { let pressureDrawingEnabled = false; let pressureDrawingSettings = { thinning: 0.3, - smoothing: 0.7, + smoothing: 0.5, streamline: 0.7 }; @@ -272,7 +272,7 @@ $(document).ready(async function () { // 載入筆壓繪圖設定 pressureDrawingSettings.thinning = parseFloat(await loadFromDB('pressureThinning') || 0.3); - pressureDrawingSettings.smoothing = parseFloat(await loadFromDB('pressureSmoothing') || 0.7); + pressureDrawingSettings.smoothing = parseFloat(await loadFromDB('pressureSmoothing') || 0.5); pressureDrawingSettings.streamline = parseFloat(await loadFromDB('pressureStreamline') || 0.7); } @@ -293,7 +293,7 @@ $(document).ready(async function () { await saveToDB('pressureThinning', 0.3); } if (await loadFromDB('pressureSmoothing') === null) { - await saveToDB('pressureSmoothing', 0.7); + await saveToDB('pressureSmoothing', 0.5); } if (await loadFromDB('pressureStreamline') === null) { await saveToDB('pressureStreamline', 0.7); @@ -499,9 +499,11 @@ $(document).ready(async function () { streamline: pressureDrawingSettings.streamline }); - if (finalStroke && backgroundImageData) { - // 恢復背景圖像 - ctx.putImageData(backgroundImageData, 0, 0); + if (finalStroke && finalStroke.length > 0) { + // 恢復背景圖像(如果有的話) + if (backgroundImageData) { + ctx.putImageData(backgroundImageData, 0, 0); + } // 繪製最終筆跡 pressureDrawing.drawStrokeOnCanvas(ctx, finalStroke, eraseMode); @@ -742,7 +744,7 @@ $(document).ready(async function () { $('#pressureThinningSlider').val(thinning); $('#pressureThinningValue').text(thinning); - const smoothing = await loadFromDB('pressureSmoothing') || 0.7; + const smoothing = await loadFromDB('pressureSmoothing') || 0.5; $('#pressureSmoothingSlider').val(smoothing); $('#pressureSmoothingValue').text(smoothing); diff --git a/pages/index.html b/pages/index.html index d073a2a..a11bd77 100644 --- a/pages/index.html +++ b/pages/index.html @@ -188,8 +188,8 @@

設定

數值越大,壓力變化對線條粗細的影響越明顯。
- - 0.7 + + 0.5 數值越大,線條越平滑。
diff --git a/pages/ja.html b/pages/ja.html index 2e2972e..699acf5 100644 --- a/pages/ja.html +++ b/pages/ja.html @@ -187,8 +187,8 @@

設定

値が大きいほど、筆圧変化が線の太さに与える影響が大きくなります。
- - 0.7 + + 0.5 値が大きいほど、線がより滑らかになります。
diff --git a/pages/pressure-drawing.js b/pages/pressure-drawing.js index 4e1f0c2..f6fd2ab 100644 --- a/pages/pressure-drawing.js +++ b/pages/pressure-drawing.js @@ -87,8 +87,7 @@ class PressureDrawing { // Finish the current stroke and return the path finishStroke(options = {}) { - if (!this.isDrawing || this.currentStroke.length < 2) { - this.isDrawing = false; + if (!this.isDrawing) { this.delayedStart = false; this.startPoint = null; return null; @@ -102,6 +101,15 @@ class PressureDrawing { this.startPoint = null; return this.generateCircularDot(strokePoints, options); } + + // 如果筆跡太短(只有起始點),也生成圓形點 + if (this.currentStroke.length < 2) { + this.isDrawing = false; + this.delayedStart = false; + const strokePoints = [...this.currentStroke]; + this.startPoint = null; + return this.generateCircularDot(strokePoints, options); + } // 動態決定是否模擬壓力 const shouldSimulatePressure = !this.hasPressureSupport && this.pressureCheckCount > 3; @@ -341,8 +349,8 @@ class PressureDrawing { const avgPressure = totalPressure / strokePoints.length; // 根據壓力計算半徑 - const baseRadius = (options.size || 12) * 0.5; - const radius = baseRadius * (0.3 + avgPressure * 0.7); // 根據壓力調整大小 + const baseRadius = (options.size || 12) * 0.6; // 增加基礎半徑 + const radius = baseRadius * (0.4 + avgPressure * 0.8); // 增加最小和最大半徑 // 生成圓形的點 const circlePoints = []; From e35813cc92d283ae506ec953f9fc2f2e8f01ecfb Mon Sep 17 00:00:00 2001 From: "Chiaki.C" Date: Thu, 10 Jul 2025 16:40:38 +0900 Subject: [PATCH 4/6] fix: hide and fixed pressure drawing settings --- pages/fontdrawer.js | 24 ++++++++++++------------ pages/index.html | 14 +++++++------- pages/ja.html | 14 +++++++------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pages/fontdrawer.js b/pages/fontdrawer.js index 86596a6..2387f0e 100644 --- a/pages/fontdrawer.js +++ b/pages/fontdrawer.js @@ -257,9 +257,9 @@ $(document).ready(async function () { const pressureDrawing = new PressureDrawing(); let pressureDrawingEnabled = false; let pressureDrawingSettings = { - thinning: 0.3, - smoothing: 0.5, - streamline: 0.7 + thinning: 0.5, + smoothing: 0.4, + streamline: 0.4 }; // 更新筆壓繪圖狀態 @@ -271,9 +271,9 @@ $(document).ready(async function () { pressureDrawingEnabled = (enabled !== 'N') && moduleInitialized; // 載入筆壓繪圖設定 - pressureDrawingSettings.thinning = parseFloat(await loadFromDB('pressureThinning') || 0.3); - pressureDrawingSettings.smoothing = parseFloat(await loadFromDB('pressureSmoothing') || 0.5); - pressureDrawingSettings.streamline = parseFloat(await loadFromDB('pressureStreamline') || 0.7); + pressureDrawingSettings.thinning = parseFloat(await loadFromDB('pressureThinning') || 0.5); + pressureDrawingSettings.smoothing = parseFloat(await loadFromDB('pressureSmoothing') || 0.4); + pressureDrawingSettings.streamline = parseFloat(await loadFromDB('pressureStreamline') || 0.4); } // 初始化 IndexedDB @@ -290,13 +290,13 @@ $(document).ready(async function () { await saveToDB('pressureDrawingEnabled', 'Y'); } if (await loadFromDB('pressureThinning') === null) { - await saveToDB('pressureThinning', 0.3); + await saveToDB('pressureThinning', 0.5); } if (await loadFromDB('pressureSmoothing') === null) { - await saveToDB('pressureSmoothing', 0.5); + await saveToDB('pressureSmoothing', 0.4); } if (await loadFromDB('pressureStreamline') === null) { - await saveToDB('pressureStreamline', 0.7); + await saveToDB('pressureStreamline', 0.4); } }).catch((error) => { console.error('IndexedDB 起動失敗', error); @@ -740,15 +740,15 @@ $(document).ready(async function () { const pressureEnabled = pressureEnabledSetting !== 'N'; // 預設啟用(除非明確設定為 N) $('#pressureDrawingEnabled').prop('checked', pressureEnabled); - const thinning = await loadFromDB('pressureThinning') || 0.3; + const thinning = await loadFromDB('pressureThinning') || 0.5; $('#pressureThinningSlider').val(thinning); $('#pressureThinningValue').text(thinning); - const smoothing = await loadFromDB('pressureSmoothing') || 0.5; + const smoothing = await loadFromDB('pressureSmoothing') || 0.4; $('#pressureSmoothingSlider').val(smoothing); $('#pressureSmoothingValue').text(smoothing); - const streamline = await loadFromDB('pressureStreamline') || 0.7; + const streamline = await loadFromDB('pressureStreamline') || 0.4; $('#pressureStreamlineSlider').val(streamline); $('#pressureStreamlineValue').text(streamline); diff --git a/pages/index.html b/pages/index.html index a11bd77..88655cc 100644 --- a/pages/index.html +++ b/pages/index.html @@ -181,20 +181,20 @@

設定

啟用後可提供更自然的筆壓效果,適合觸控筆或支援筆壓的設備。不支援的設備也會自動模擬筆壓繪圖。
-
+
diff --git a/pages/ja.html b/pages/ja.html index 699acf5..64ba69a 100644 --- a/pages/ja.html +++ b/pages/ja.html @@ -180,20 +180,20 @@

設定

有効にすると、タッチペンや筆圧対応デバイスでより自然な筆圧効果を得られます。筆圧不対応の設備でも自動的に筆圧繪圖を模擬します。
-
+
From 2e9848bf803ebadf2f5c6c52709d178199fef5b7 Mon Sep 17 00:00:00 2001 From: but Date: Thu, 10 Jul 2025 16:38:57 +0800 Subject: [PATCH 5/6] hide something --- pages/fontdrawer.js | 4 ++-- pages/index.html | 11 ++--------- pages/ja.html | 15 ++++----------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/pages/fontdrawer.js b/pages/fontdrawer.js index 2387f0e..7865668 100644 --- a/pages/fontdrawer.js +++ b/pages/fontdrawer.js @@ -753,7 +753,7 @@ $(document).ready(async function () { $('#pressureStreamlineValue').text(streamline); // 控制筆壓設定區域的顯示/隱藏 - $('#pressureSettings').toggle(pressureEnabled); + // $('#pressureSettings').toggle(pressureEnabled); $('#spanAllCount').text(Object.keys(glyphMap).length); $('#spanDoneCount').text(await countGlyphFromDB()); @@ -778,7 +778,7 @@ $(document).ready(async function () { $('#pressureDrawingEnabled').on('change', async function () { const enabled = $(this).prop('checked'); saveToDB('pressureDrawingEnabled', enabled ? 'Y' : 'N'); - $('#pressureSettings').toggle(enabled); + //$('#pressureSettings').toggle(enabled); // 立即更新筆壓繪圖狀態 await updatePressureDrawingStatus(); }); diff --git a/pages/index.html b/pages/index.html index 88655cc..9375566 100644 --- a/pages/index.html +++ b/pages/index.html @@ -42,13 +42,6 @@ canvas { width: 320px; height: 320px; touch-action: none; } } - /* 九宮格底圖樣式 */ - #gridCanvas { - background-image: url('grid.png'); /* 替換為您的九宮格底圖 */ - background-size: cover; - background-repeat: no-repeat; - - } #gridCanvas { z-index: 0 } /* 九宮格底圖在下方 */ #drawingCanvas { position: absolute; top: 0; left: 0; z-index: 1 } /* 繪圖畫布在上方 */ @@ -81,6 +74,7 @@ #settings-container input[type="range"] { width: 80%; vertical-align: middle; } #settings-container input[type="checkbox"] { width: 2em; height: 2em } #settings-container .note { display: block; color: #ccc } + #pressureSettings { display: none; /*margin-left: 20px;*/ } #hintContent { padding: 0 1.2em } #hintContent li { margin: 10px 0; line-height: 1.6; } @@ -176,12 +170,11 @@

設定

設定為不等寬會更像手寫字型,但可能不適合用於直排。
-
啟用後可提供更自然的筆壓效果,適合觸控筆或支援筆壓的設備。不支援的設備也會自動模擬筆壓繪圖。
-