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