摘要:相反,當(dāng)響應(yīng)指針事件時,它會調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分。回調(diào)函數(shù)可能會返回另一個回調(diào)函數(shù),以便在按下按鈕并且將指針移動到另一個像素時得到通知。它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Pixel Art Editor
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
我看著眼前的許多顏色。 我看著我的空白畫布。 然后,我嘗試使用顏色,就像形成詩歌的詞語,就像塑造音樂的音符。
Joan Miro
前面幾章的內(nèi)容為你提供了構(gòu)建基本的 Web 應(yīng)用所需的所有元素。 在本章中,我們將實現(xiàn)一個。
我們的應(yīng)用將是像素繪圖程序,你可以通過操縱放大視圖(正方形彩色網(wǎng)格),來逐像素修改圖像。 你可以使用它來打開圖像文件,用鼠標(biāo)或其他指針設(shè)備在它們上面涂畫并保存。 這是它的樣子:
在電腦上繪畫很棒。 你不需要擔(dān)心材料,技能或天賦。 你只需要開始涂畫。
組件應(yīng)用的界面在頂部顯示大的元素,在它下面有許多表單字段。 用戶通過從字段中選擇工具,然后單擊,觸摸或拖動畫布來繪制圖片。 有用于繪制單個像素或矩形,填充區(qū)域以及從圖片中選取顏色的工具。
我們將編輯器界面構(gòu)建為多個組件和對象,負(fù)責(zé) DOM 的一部分,并可能在其中包含其他組件。
應(yīng)用的狀態(tài)由當(dāng)前圖片,所選工具和所選顏色組成。 我們將建立一些東西,以便狀態(tài)存在于單一的值中,并且界面組件總是基于當(dāng)前狀態(tài)下他們看上去的樣子。
為了明白為什么這很重要,讓我們考慮替代方案:將狀態(tài)片段分配給整個界面。 直到某個時期,這更容易編寫。 我們可以放入顏色字段,并在需要知道當(dāng)前顏色時讀取其值。
但是,我們添加了顏色選擇器。它是一種工具,可讓你單擊圖片來選擇給定像素的顏色。 為了保持顏色字段顯示正確的顏色,該工具必須知道它存在,并在每次選擇新顏色時對其進(jìn)行更新。 如果你添加了另一個讓顏色可見的地方(也許鼠標(biāo)光標(biāo)可以顯示它),你必須更新你的改變顏色的代碼來保持同步。
實際上,這會讓你遇到一個問題,即界面的每個部分都需要知道所有其他部分,它們并不是非常模塊化的。 對于本章中的小應(yīng)用,這可能不成問題。 對于更大的項目,它可能變成真正的噩夢。
所以為了在原則上避免這種噩夢,我們將對數(shù)據(jù)流非常嚴(yán)格。 存在一個狀態(tài),界面根據(jù)該狀態(tài)繪制。 界面組件可以通過更新狀態(tài)來響應(yīng)用戶動作,此時組件有機(jī)會與新的狀態(tài)進(jìn)行同步。
在實踐中,每個組件的建立,都是為了在給定一個新的狀態(tài)時,它還會通知它的子組件,只要這些組件需要更新。 建立這個有點麻煩。 讓這個更方便是許多瀏覽器編程庫的主要賣點。 但對于像這樣的小應(yīng)用,我們可以在沒有這種基礎(chǔ)設(shè)施的情況下完成。
狀態(tài)更新表示為對象,我們將其稱為動作。 組件可以創(chuàng)建這樣的動作并分派它們 - 將它們給予中央狀態(tài)管理函數(shù)。 該函數(shù)計算下一個狀態(tài),之后界面組件將自己更新為這個新狀態(tài)。
我們正在執(zhí)行一個混亂的任務(wù),運行一個用戶界面并對其應(yīng)用一些結(jié)構(gòu)。 盡管與 DOM 相關(guān)的部分仍然充滿了副作用,但它們由一個概念上簡單的主干支撐 - 狀態(tài)更新循環(huán)。 狀態(tài)決定了 DOM 的外觀,而 DOM 事件可以改變狀態(tài)的唯一方法,是向狀態(tài)分派動作。
這種方法有許多變種,每個變種都有自己的好處和問題,但它們的中心思想是一樣的:狀態(tài)變化應(yīng)該通過明確定義的渠道,而不是遍布整個地方。
我們的組件將是與界面一致的類。 他們的構(gòu)造器被賦予一個狀態(tài),它可能是整個應(yīng)用狀態(tài),或者如果它不需要訪問所有東西,是一些較小的值,并使用它構(gòu)建一個dom屬性,也就是表示組件的 DOM。 大多數(shù)構(gòu)造器還會接受一些其他值,這些值不會隨著時間而改變,例如它們可用于分派操作的函數(shù)。
每個組件都有一個setState方法,用于將其同步到新的狀態(tài)值。 該方法接受一個參數(shù),該參數(shù)的類型與構(gòu)造器的第一個參數(shù)的類型相同。
狀態(tài)應(yīng)用狀態(tài)將是一個帶有圖片,工具和顏色屬性的對象。 圖片本身就是一個對象,存儲圖片的寬度,高度和像素內(nèi)容。 像素逐行存儲在一個數(shù)組中,方式與第 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); } }
我們希望能夠?qū)D片當(dāng)做不變的值,我們將在本章后面回顧其原因。 但是我們有時也需要一次更新大量像素。 為此,該類有draw方法,接受更新后的像素(具有x,y和color屬性的對象)的數(shù)組,并創(chuàng)建一個覆蓋這些像素的新圖像。 此方法使用不帶參數(shù)的slice來復(fù)制整個像素數(shù)組 - 切片的起始位置默認(rèn)為 0,結(jié)束位置為數(shù)組的長度。
empty 方法使用我們以前沒有見過的兩個數(shù)組功能。 可以使用數(shù)字調(diào)用Array構(gòu)造器來創(chuàng)建給定長度的空數(shù)組。 然后fill方法可以用于使用給定值填充數(shù)組。 這些用于創(chuàng)建一個數(shù)組,所有像素具有相同顏色。
顏色存儲為字符串,包含傳統(tǒng) CSS 顏色代碼 - 一個井號(#),后跟六個十六進(jìn)制數(shù)字,兩個用于紅色分量,兩個用于綠色分量,兩個用于藍(lán)色分量。這是一種有點神秘而不方便的顏色編寫方法,但它是 HTML 顏色輸入字段使用的格式,并且可以在canvas繪圖上下文的fillColor屬性中使用,所以對于我們在程序中使用顏色的方式,它足夠?qū)嵱谩?/p>
所有分量都為零的黑色寫成"#000000",亮粉色看起來像#ff00ff",其中紅色和藍(lán)色分量的最大值為 255,以十六進(jìn)制數(shù)字寫為ff(a到f用作數(shù)字 10 到 15)。
我們將允許界面將動作分派為對象,它是屬性覆蓋先前狀態(tài)的屬性。當(dāng)用戶改變顏色字段時,顏色字段可以分派像{color: field.value}這樣的對象,從這個對象可以計算出一個新的狀態(tài)。
function updateState(state, action) { return Object.assign({}, state, action); }
這是相當(dāng)麻煩的模式,其中Object.assign用于首先將狀態(tài)屬性添加到空對象,然后使用來自動作的屬性覆蓋其中的一些屬性,這在使用不可變對象的 JavaScript 代碼中很常見。 一個更方便的表示法處于標(biāo)準(zhǔn)化的最后階段,也就是在對象表達(dá)式中使用三點運算符來包含另一個對象的所有屬性。 有了這個補充,你可以寫出{...state, ...action}。 在撰寫本文時,這還不適用于所有瀏覽器。
DOM 的構(gòu)建界面組件做的主要事情之一是創(chuàng)建 DOM 結(jié)構(gòu)。 我們再也不想直接使用冗長的 DOM 方法,所以這里是elt函數(shù)的一個稍微擴(kuò)展的版本。
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 章中使用的版本之間的主要區(qū)別在于,它將屬性(property)分配給 DOM 節(jié)點,而不是屬性(attribute)。 這意味著我們不能用它來設(shè)置任意屬性(attribute),但是我們可以用它來設(shè)置值不是字符串的屬性(property),比如onclick,可以將它設(shè)置為一個函數(shù),來注冊點擊事件處理器。
這允許這種注冊事件處理器的方式:
畫布
我們要定義的第一個組件是界面的一部分,它將圖片顯示為彩色框的網(wǎng)格。 該組件負(fù)責(zé)兩件事:顯示圖片并將該圖片上的指針事件傳給應(yīng)用的其余部分。
因此,我們可以將其定義為僅了解當(dāng)前圖片,而不是整個應(yīng)用狀態(tài)的組件。 因為它不知道整個應(yīng)用是如何工作的,所以不能直接發(fā)送操作。 相反,當(dāng)響應(yīng)指針事件時,它會調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分。
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的正方形,由比例常數(shù)決定。 為了避免不必要的工作,該組件會跟蹤其當(dāng)前圖片,并且僅當(dāng)將setState賦予新圖片時才會重繪。
實際的繪圖功能根據(jù)比例和圖片大小設(shè)置畫布大小,并用一系列正方形填充它,每個像素一個。
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); } } }
當(dāng)鼠標(biāo)懸停在圖片畫布上,并且按下鼠標(biāo)左鍵時,組件調(diào)用pointerDown回調(diào)函數(shù),提供被點擊圖片坐標(biāo)的像素位置。 這將用于實現(xiàn)鼠標(biāo)與圖片的交互。 回調(diào)函數(shù)可能會返回另一個回調(diào)函數(shù),以便在按下按鈕并且將指針移動到另一個像素時得到通知。
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來查找畫布在屏幕上的位置,所以可以將鼠標(biāo)事件坐標(biāo)(clientX和clientY)轉(zhuǎn)換為圖片坐標(biāo)。 它們總是向下取舍,以便它們指代特定的像素。
對于觸摸事件,我們必須做類似的事情,但使用不同的事件,并確保我們在"touchstart"事件中調(diào)用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屬性中使用第一個觸摸對象的坐標(biāo)。
應(yīng)用為了能夠逐步構(gòu)建應(yīng)用,我們將主要組件實現(xiàn)為畫布周圍的外殼,以及一組動態(tài)工具和控件,我們將其傳遞給其構(gòu)造器。
控件是出現(xiàn)在圖片下方的界面元素。 它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。
工具是繪制像素或填充區(qū)域的東西。 該應(yīng)用將一組可用工具顯示為字段。 當(dāng)前選擇的工具決定了,當(dāng)用戶使用指針設(shè)備與圖片交互時,發(fā)生的事情。 它們作為一個對象而提供,該對象將出現(xiàn)在下拉字段中的名稱,映射到實現(xiàn)這些工具的函數(shù)。 這個函數(shù)接受圖片位置,當(dāng)前應(yīng)用狀態(tài)和dispatch函數(shù)作為參數(shù)。 它們可能會返回一個移動處理器,當(dāng)指針移動到另一個像素時,使用新位置和當(dāng)前狀態(tài)調(diào)用該函數(shù)。
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的指針處理器,使用適當(dāng)?shù)膮?shù)調(diào)用當(dāng)前選定的工具,如果返回了移動處理器,使其也接收狀態(tài)。
所有控件在this.controls中構(gòu)造并存儲,以便在應(yīng)用狀態(tài)更改時更新它們。 reduce的調(diào)用會在控件的 DOM 元素之間引入空格。 這樣他們看起來并不那么密集。
第一個控件是工具選擇菜單。 它創(chuàng)建元素,每個工具帶有一個選項,并設(shè)置"change"事件處理器,用于在用戶選擇不同的工具時更新應(yīng)用狀態(tài)。
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, "
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105044.html
摘要:事件與節(jié)點每個瀏覽器事件處理器被注冊在上下文中。事件對象雖然目前為止我們忽略了它,事件處理器函數(shù)作為對象傳遞事件對象。若事件處理器不希望執(zhí)行默認(rèn)行為通常是因為已經(jīng)處理了該事件,會調(diào)用事件對象的方法。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Handling Events 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會是一個活動,其中興趣相同的人聚在一起,針對他們所知的事情進(jìn)行小型非正式的展示。所有接口均以路徑為中心。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: Skill-Sharing Website 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...
摘要:在其沙箱中提供了將文本轉(zhuǎn)換成文檔對象模型的功能。瀏覽器使用與該形狀對應(yīng)的數(shù)據(jù)結(jié)構(gòu)來表示文檔。我們將這種表示方式稱為文檔對象模型,或簡稱。樹回想一下第章中提到的語法樹。語言的語法樹有標(biāo)識符值和應(yīng)用節(jié)點。元素表示標(biāo)簽的節(jié)點用于確定文檔結(jié)構(gòu)。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:The Document Object Model 譯者:飛龍 協(xié)議...
摘要:貝塞爾曲線方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個控制點而不是一個,線段的每一個端點都需要一個控制點。下面是描述貝塞爾曲線的簡單示例。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Drawing on Canvas 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2...
閱讀 3109·2023-04-25 16:50
閱讀 915·2021-11-25 09:43
閱讀 3528·2021-09-26 10:11
閱讀 2526·2019-08-26 13:28
閱讀 2537·2019-08-26 13:23
閱讀 2431·2019-08-26 11:53
閱讀 3576·2019-08-23 18:19
閱讀 2996·2019-08-23 16:27