摘要:相反,當響應指針事件時,它會調用創建它的代碼提供的回調函數,該函數將處理應用的特定部分。回調函數可能會返回另一個回調函數,以便在按下按鈕并且將指針移動到另一個像素時得到通知。它們為組件構造器的數組而提供。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Pixel Art Editor
譯者:飛龍
協議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
我看著眼前的許多顏色。 我看著我的空白畫布。 然后,我嘗試使用顏色,就像形成詩歌的詞語,就像塑造音樂的音符。
Joan Miro
前面幾章的內容為你提供了構建基本的 Web 應用所需的所有元素。 在本章中,我們將實現一個。
我們的應用將是像素繪圖程序,你可以通過操縱放大視圖(正方形彩色網格),來逐像素修改圖像。 你可以使用它來打開圖像文件,用鼠標或其他指針設備在它們上面涂畫并保存。 這是它的樣子:
在電腦上繪畫很棒。 你不需要擔心材料,技能或天賦。 你只需要開始涂畫。
組件應用的界面在頂部顯示大的元素,在它下面有許多表單字段。 用戶通過從字段中選擇工具,然后單擊,觸摸或拖動畫布來繪制圖片。 有用于繪制單個像素或矩形,填充區域以及從圖片中選取顏色的工具。
我們將編輯器界面構建為多個組件和對象,負責 DOM 的一部分,并可能在其中包含其他組件。
應用的狀態由當前圖片,所選工具和所選顏色組成。 我們將建立一些東西,以便狀態存在于單一的值中,并且界面組件總是基于當前狀態下他們看上去的樣子。
為了明白為什么這很重要,讓我們考慮替代方案:將狀態片段分配給整個界面。 直到某個時期,這更容易編寫。 我們可以放入顏色字段,并在需要知道當前顏色時讀取其值。
但是,我們添加了顏色選擇器。它是一種工具,可讓你單擊圖片來選擇給定像素的顏色。 為了保持顏色字段顯示正確的顏色,該工具必須知道它存在,并在每次選擇新顏色時對其進行更新。 如果你添加了另一個讓顏色可見的地方(也許鼠標光標可以顯示它),你必須更新你的改變顏色的代碼來保持同步。
實際上,這會讓你遇到一個問題,即界面的每個部分都需要知道所有其他部分,它們并不是非常模塊化的。 對于本章中的小應用,這可能不成問題。 對于更大的項目,它可能變成真正的噩夢。
所以為了在原則上避免這種噩夢,我們將對數據流非常嚴格。 存在一個狀態,界面根據該狀態繪制。 界面組件可以通過更新狀態來響應用戶動作,此時組件有機會與新的狀態進行同步。
在實踐中,每個組件的建立,都是為了在給定一個新的狀態時,它還會通知它的子組件,只要這些組件需要更新。 建立這個有點麻煩。 讓這個更方便是許多瀏覽器編程庫的主要賣點。 但對于像這樣的小應用,我們可以在沒有這種基礎設施的情況下完成。
狀態更新表示為對象,我們將其稱為動作。 組件可以創建這樣的動作并分派它們 - 將它們給予中央狀態管理函數。 該函數計算下一個狀態,之后界面組件將自己更新為這個新狀態。
我們正在執行一個混亂的任務,運行一個用戶界面并對其應用一些結構。 盡管與 DOM 相關的部分仍然充滿了副作用,但它們由一個概念上簡單的主干支撐 - 狀態更新循環。 狀態決定了 DOM 的外觀,而 DOM 事件可以改變狀態的唯一方法,是向狀態分派動作。
這種方法有許多變種,每個變種都有自己的好處和問題,但它們的中心思想是一樣的:狀態變化應該通過明確定義的渠道,而不是遍布整個地方。
我們的組件將是與界面一致的類。 他們的構造器被賦予一個狀態,它可能是整個應用狀態,或者如果它不需要訪問所有東西,是一些較小的值,并使用它構建一個dom屬性,也就是表示組件的 DOM。 大多數構造器還會接受一些其他值,這些值不會隨著時間而改變,例如它們可用于分派操作的函數。
每個組件都有一個setState方法,用于將其同步到新的狀態值。 該方法接受一個參數,該參數的類型與構造器的第一個參數的類型相同。
狀態應用狀態將是一個帶有圖片,工具和顏色屬性的對象。 圖片本身就是一個對象,存儲圖片的寬度,高度和像素內容。 像素逐行存儲在一個數組中,方式與第 6 章中的矩陣類相同,按行存儲,從上到下。
class Picture { constructor(width, height, pixels) { this.width = width; this.height = height; this.pixels = pixels; } static empty(width, height, color) { let pixels = new Array(width * height).fill(color); return new Picture(width, height, pixels); } pixel(x, y) { return this.pixels[x + y * this.width]; } draw(pixels) { let copy = this.pixels.slice(); for (let {x, y, color} of pixels) { copy[x + y * this.width] = color; } return new Picture(this.width, this.height, copy); } }
我們希望能夠將圖片當做不變的值,我們將在本章后面回顧其原因。 但是我們有時也需要一次更新大量像素。 為此,該類有draw方法,接受更新后的像素(具有x,y和color屬性的對象)的數組,并創建一個覆蓋這些像素的新圖像。 此方法使用不帶參數的slice來復制整個像素數組 - 切片的起始位置默認為 0,結束位置為數組的長度。
empty 方法使用我們以前沒有見過的兩個數組功能。 可以使用數字調用Array構造器來創建給定長度的空數組。 然后fill方法可以用于使用給定值填充數組。 這些用于創建一個數組,所有像素具有相同顏色。
顏色存儲為字符串,包含傳統 CSS 顏色代碼 - 一個井號(#),后跟六個十六進制數字,兩個用于紅色分量,兩個用于綠色分量,兩個用于藍色分量。這是一種有點神秘而不方便的顏色編寫方法,但它是 HTML 顏色輸入字段使用的格式,并且可以在canvas繪圖上下文的fillColor屬性中使用,所以對于我們在程序中使用顏色的方式,它足夠實用。
所有分量都為零的黑色寫成"#000000",亮粉色看起來像#ff00ff",其中紅色和藍色分量的最大值為 255,以十六進制數字寫為ff(a到f用作數字 10 到 15)。
我們將允許界面將動作分派為對象,它是屬性覆蓋先前狀態的屬性。當用戶改變顏色字段時,顏色字段可以分派像{color: field.value}這樣的對象,從這個對象可以計算出一個新的狀態。
function updateState(state, action) { return Object.assign({}, state, action); }
這是相當麻煩的模式,其中Object.assign用于首先將狀態屬性添加到空對象,然后使用來自動作的屬性覆蓋其中的一些屬性,這在使用不可變對象的 JavaScript 代碼中很常見。 一個更方便的表示法處于標準化的最后階段,也就是在對象表達式中使用三點運算符來包含另一個對象的所有屬性。 有了這個補充,你可以寫出{...state, ...action}。 在撰寫本文時,這還不適用于所有瀏覽器。
DOM 的構建界面組件做的主要事情之一是創建 DOM 結構。 我們再也不想直接使用冗長的 DOM 方法,所以這里是elt函數的一個稍微擴展的版本。
function elt(type, props, ...children) { let dom = document.createElement(type); if (props) Object.assign(dom, props); for (let child of children) { if (typeof child != "string") dom.appendChild(child); else dom.appendChild(document.createTextNode(child)); } return dom; }
這個版本與我們在第 16 章中使用的版本之間的主要區別在于,它將屬性(property)分配給 DOM 節點,而不是屬性(attribute)。 這意味著我們不能用它來設置任意屬性(attribute),但是我們可以用它來設置值不是字符串的屬性(property),比如onclick,可以將它設置為一個函數,來注冊點擊事件處理器。
這允許這種注冊事件處理器的方式:
畫布
我們要定義的第一個組件是界面的一部分,它將圖片顯示為彩色框的網格。 該組件負責兩件事:顯示圖片并將該圖片上的指針事件傳給應用的其余部分。
因此,我們可以將其定義為僅了解當前圖片,而不是整個應用狀態的組件。 因為它不知道整個應用是如何工作的,所以不能直接發送操作。 相反,當響應指針事件時,它會調用創建它的代碼提供的回調函數,該函數將處理應用的特定部分。
const scale = 10; class PictureCanvas { constructor(picture, pointerDown) { this.dom = elt("canvas", { onmousedown: event => this.mouse(event, pointerDown), ontouchstart: event => this.touch(event, pointerDown) }); drawPicture(picture, this.dom, scale); } setState(picture) { if (this.picture == picture) return; this.picture = picture; drawPicture(this.picture, this.dom, scale); } }
我們將每個像素繪制成一個10x10的正方形,由比例常數決定。 為了避免不必要的工作,該組件會跟蹤其當前圖片,并且僅當將setState賦予新圖片時才會重繪。
實際的繪圖功能根據比例和圖片大小設置畫布大小,并用一系列正方形填充它,每個像素一個。
function drawPicture(picture, canvas, scale) { canvas.width = picture.width * scale; canvas.height = picture.height * scale; let cx = canvas.getContext("2d"); for (let y = 0; y < picture.height; y++) { for (let x = 0; x < picture.width; x++) { cx.fillStyle = picture.pixel(x, y); cx.fillRect(x * scale, y * scale, scale, scale); } } }
當鼠標懸停在圖片畫布上,并且按下鼠標左鍵時,組件調用pointerDown回調函數,提供被點擊圖片坐標的像素位置。 這將用于實現鼠標與圖片的交互。 回調函數可能會返回另一個回調函數,以便在按下按鈕并且將指針移動到另一個像素時得到通知。
PictureCanvas.prototype.mouse = function(downEvent, onDown) { if (downEvent.button != 0) return; let pos = pointerPosition(downEvent, this.dom); let onMove = onDown(pos); if (!onMove) return; let move = moveEvent => { if (moveEvent.buttons == 0) { this.dom.removeEventListener("mousemove", move); } else { let newPos = pointerPosition(moveEvent, this.dom); if (newPos.x == pos.x && newPos.y == pos.y) return; pos = newPos; onMove(newPos); } }; this.dom.addEventListener("mousemove", move); }; function pointerPosition(pos, domNode) { let rect = domNode.getBoundingClientRect(); return {x: Math.floor((pos.clientX - rect.left) / scale), y: Math.floor((pos.clientY - rect.top) / scale)}; }
由于我們知道像素的大小,我們可以使用getBoundingClientRect來查找畫布在屏幕上的位置,所以可以將鼠標事件坐標(clientX和clientY)轉換為圖片坐標。 它們總是向下取舍,以便它們指代特定的像素。
對于觸摸事件,我們必須做類似的事情,但使用不同的事件,并確保我們在"touchstart"事件中調用preventDefault以防止滑動。
PictureCanvas.prototype.touch = function(startEvent, onDown) { let pos = pointerPosition(startEvent.touches[0], this.dom); let onMove = onDown(pos); startEvent.preventDefault(); if (!onMove) return; let move = moveEvent => { let newPos = pointerPosition(moveEvent.touches[0], this.dom); if (newPos.x == pos.x && newPos.y == pos.y) return; pos = newPos; onMove(newPos); }; let end = () => { this.dom.removeEventListener("touchmove", move); this.dom.removeEventListener("touchend", end); }; this.dom.addEventListener("touchmove", move); this.dom.addEventListener("touchend", end); };
對于觸摸事件,clientX和clientY不能直接在事件對象上使用,但我們可以在touches屬性中使用第一個觸摸對象的坐標。
應用為了能夠逐步構建應用,我們將主要組件實現為畫布周圍的外殼,以及一組動態工具和控件,我們將其傳遞給其構造器。
控件是出現在圖片下方的界面元素。 它們為組件構造器的數組而提供。
工具是繪制像素或填充區域的東西。 該應用將一組可用工具顯示為字段。 當前選擇的工具決定了,當用戶使用指針設備與圖片交互時,發生的事情。 它們作為一個對象而提供,該對象將出現在下拉字段中的名稱,映射到實現這些工具的函數。 這個函數接受圖片位置,當前應用狀態和dispatch函數作為參數。 它們可能會返回一個移動處理器,當指針移動到另一個像素時,使用新位置和當前狀態調用該函數。
class PixelEditor { constructor(state, config) { let {tools, controls, dispatch} = config; this.state = state; this.canvas = new PictureCanvas(state.picture, pos => { let tool = tools[this.state.tool]; let onMove = tool(pos, this.state, dispatch); if (onMove) return pos => onMove(pos, this.state); }); this.controls = controls.map( Control => new Control(state, config)); this.dom = elt("div", {}, this.canvas.dom, elt("br"), ...this.controls.reduce( (a, c) => a.concat(" ", c.dom), [])); } setState(state) { this.state = state; this.canvas.setState(state.picture); for (let ctrl of this.controls) ctrl.setState(state); } }
指定給PictureCanvas的指針處理器,使用適當的參數調用當前選定的工具,如果返回了移動處理器,使其也接收狀態。
所有控件在this.controls中構造并存儲,以便在應用狀態更改時更新它們。 reduce的調用會在控件的 DOM 元素之間引入空格。 這樣他們看起來并不那么密集。
第一個控件是工具選擇菜單。 它創建元素,每個工具帶有一個選項,并設置"change"事件處理器,用于在用戶選擇不同的工具時更新應用狀態。
class ToolSelect { constructor(state, {tools, dispatch}) { this.select = elt("select", { onchange: () => dispatch({tool: this.select.value}) }, ...Object.keys(tools).map(name => elt("option", { selected: name == state.tool }, name))); this.dom = elt("label", null, "
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105044.html
摘要:事件與節點每個瀏覽器事件處理器被注冊在上下文中。事件對象雖然目前為止我們忽略了它,事件處理器函數作為對象傳遞事件對象。若事件處理器不希望執行默認行為通常是因為已經處理了該事件,會調用事件對象的方法。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Handling Events 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關于指導電腦的書。在可控的范圍內編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數字存儲在內存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會是一個活動,其中興趣相同的人聚在一起,針對他們所知的事情進行小型非正式的展示。所有接口均以路徑為中心。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: Skill-Sharing Website 譯者:飛龍 協議:CC BY-NC-SA 4...
摘要:在其沙箱中提供了將文本轉換成文檔對象模型的功能。瀏覽器使用與該形狀對應的數據結構來表示文檔。我們將這種表示方式稱為文檔對象模型,或簡稱。樹回想一下第章中提到的語法樹。語言的語法樹有標識符值和應用節點。元素表示標簽的節點用于確定文檔結構。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:The Document Object Model 譯者:飛龍 協議...
摘要:貝塞爾曲線方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個控制點而不是一個,線段的每一個端點都需要一個控制點。下面是描述貝塞爾曲線的簡單示例。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Drawing on Canvas 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2...
閱讀 3069·2023-04-25 16:50
閱讀 903·2021-11-25 09:43
閱讀 3512·2021-09-26 10:11
閱讀 2517·2019-08-26 13:28
閱讀 2530·2019-08-26 13:23
閱讀 2418·2019-08-26 11:53
閱讀 3566·2019-08-23 18:19
閱讀 2986·2019-08-23 16:27