Indonesian (Bahasa Indonesia) translation by Keti Pritania (you can also view the original English article)
Ketika HTML5 menjadi lebih populer, lebih banyak browser utama mulai mendukung API-nya. Hari ini, menggunakan Canvas and File APIs, kita dapat membuat editor gambar lengkap, dengan fitur yang setara dengan beberapa aplikasi desktop. Untuk ini, kita akan menggunakan pustaka EaselJS. Ia menggunakan sintaks yang mirip dengan AS3, sehingga akan mudah dipahami baik untuk pemrogram Flash dan JavaScript.
Pratinjau Hasil Akhir
Mari kita lihat hasil akhir yang akan kita kerjakan:



Klik untuk mencoba demo
Bermain-main dengannya untuk merasakan apa yang bisa dilakukannya. Anda bahkan mungkin ingin mengunduh kode sumber lengkap dan melihat-lihat sebelum menggali ke dalam tutorial ini.
Pengenalan
Karena jumlah kode dalam tutorial ini, aku akan pergi melalui masing-masing file dan menjelaskan setiap bagian pada gilirannya, daripada membimbing Anda melalui pembangunan kembali dari awal. Saya akan mencoba untuk komentar semuanya sebanyak yang saya bisa, dan saya percaya Anda akan mengerti segala sesuatu.
Langkah 1: Gaya
Saya akan mulai dalam cara yang tidak biasa, dari file CSS. Pertama membuat style.css file:
* { font-family: Calibri, Sans-serif; outline: none; } body, html { margin: 0; padding: 0; overflow: hidden; background: url(background.gif); } canvas { clear: both; display: block; z-index: -1; } input { background: white; border: 1px solid black; border-radius: 4px; padding: 3px 5px; } input[type=text] { padding: 5px; } button { padding: 6px 10px; color: rgb(255, 255, 255); background: rgba(0, 0, 0, 0.3); border: 1px solid rgb(40, 40, 40); border-radius: 4px; } button:hover, button.hover { background: rgba(0, 0, 0, 0.2); } button:active, button.active { background: rgba(0, 0, 0, 0.4); } ul#mainmenu { list-style: none; padding: 2px; margin: 0; float: left; background: rgb(150, 150, 150); font-size: 1.2em; width: 778px; } ul#mainmenu li { float: left; margin: 0; padding: 0 2px 0 0; position: relative; } ul#mainmenu li button { float: left; } ul#mainmenu li ul.submenu { list-style: none; position: absolute; left: -2px; top: 34px; background: rgb(150, 150, 150); margin: 0; padding: 0; display: none; float: left; width: 170px; border-radius: 0 0 4px 4px; } ul#mainmenu li ul.submenu li { margin: 0; padding: 0; clear: both; width: 170px; } ul#mainmenu li ul.submenu li button { width: 162px; text-align: left; margin: 4px; padding-left: 20px; } ul#mainmenu li ul.submenu li button:hover { background: rgba(0, 0, 0, 0.2); } ul#mainmenu li ul.submenu li button:active { background: rgba(0, 0, 0, 0.4); } ul#mainmenu li ul.submenu li hr { margin: 4px; border-top: 1px solid rgba(0, 0, 0, 0.4); border-bottom: 1px solid rgb(205, 205, 205); } div#overlay { background: rgba(0, 0, 0, 0.6); position: fixed; width: 100%; height: 100%; top: 0; left: 0; z-index: 1; display: none; } ul#layers { width: 232px; position: fixed; right: 0; top: 37px; background: rgb(150, 150, 150); border-top: 1px dotted rgb(100, 100, 100); list-style: none; margin: 0; padding: 0 5px 5px; overflow-y: auto; overflow-x: hidden; } ul#layers li { margin-top: 5px; padding: 5px; background: rgb(180, 180, 180); border-radius: 4px; } ul#layers li.active { background: rgb(160, 160, 160); padding: 3px; border: 2px dotted black; } ul#layers li img { width: 42px; height: 42px; float: left; padding: 2px; color: rgb(255, 255, 255); background: rgba(0, 0, 0, 0.3); border: 1px solid rgb(40, 40, 40); border-radius: 4px; } ul#layers li h1 { font-size: 16px; padding: 0 5px; margin: 3px 0; width: 132px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } ul#layers li span { padding: 0 5px; margin: 3px 0; width: 132px; } ul#layers li span button { padding: 3px 5px; margin: 0 3px 0 0; } button#button-move, #button-select, #button-text { background-repeat: no-repeat; background-position: 50% 50%; padding: 6px 15px; } button#button-move { background-image: url(move.gif); } button#button-text { background-image: url(text.gif); } button#button-openfile input, button#button-importfile input { position: relative; left: -23px; top: -8px; width: 162px; opacity: 0; } div#dialog-tooltext select { padding: 4px; width: 173px; color: rgb(0, 0, 0); border: 1px solid rgb(40, 40, 40); border-radius: 4px; } div.dialog { border: 1px solid black; background: rgb(240, 240, 240); position: fixed; border-radius: 4px; z-index: 2; display: none; padding: 40px; } div#cropoverlay { position: fixed; left: 0; top: 0; z-index: 2; background: rgba(255, 255, 255, 0.15); width: 120px; height: 120px; border: 1px dotted black; border-radius: 0; padding: 0; display: none; } div#cropoverlay div { width: 20px; height: 20px; position: absolute; z-index: 1000; right: 0; bottom: 0; border-top: 1px solid black; border-left: 1px solid black; } .ui-resizable { position: relative;} .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
Pada baris pertama kami mengubah font dan menonaktifkan menguraikan elemen. Selanjutnya ada hanya gaya definisi: ul #mainmenu adalah elemen menu utama, div #overlay naungan di bawah semua dialog, dan ul #layers adalah panel lapisan yang akan ditampilkan di sisi kanan kanvas. Selanjutnya kita mendefinisikan gaya untuk tombol alat, dan akhirnya kita memiliki sebuah fragmen dari gaya jQuery-UI, karena kita akan membutuhkan bagian ini untuk dialog pemangkasan layer.
Selanjutnya muncul file print.css yang hanya berisi dua baris untuk menyembunyikan semuanya terpisah dari kanvas saat mencetak gambar (gaya ini hanya diterapkan ketika Anda mencetak halaman, karena deklarasinya dalam file HTML).
body * { visibility: hidden; } canvas { visibility: visible; position: absolute; top: 0; left: 0; }
Baris pertama menyembunyikan semua elemen dalam bagian tubuh, dan baris kedua membuat hanya kanvas terlihat (dan juga selaras dengan sudut kiri atas). Hal ini karena ketika seseorang ingin mencetak foto mereka biasanya tidak ingin mencetak antarmuka.
Langkah 2: Struktur HTML
Anda harus memiliki ide dasar dari antarmuka dari melihat CSS file di atas. Sekarang membuat file index.html tersebut dan masukkan baris berikut:
<!DOCTYPE html> <html> <head> <!-- CSS Styles --> <link rel="stylesheet" type="text/css" href="style.css"/> <link rel="stylesheet" type="text/css" media="print" href="print.css"/> <!-- jQuery & jQuery-UI's Selectable + Draggable --> <script type="text/JavaScript" src="jquery-1.7.1.min.js"></script> <script type="text/JavaScript" src="jquery-ui-1.8.18.custom.min.js"></script> <!-- EaselJS Canvas Interface --> <script type="text/JavaScript" src="easel.js"></script> <!-- EaselJS' Built-in Filters --> <script type="text/JavaScript" src="ColorFilter.js"></script> <script type="text/JavaScript" src="ColorMatrixFilter.js"></script> <!-- Main Application Structure --> <script type="text/JavaScript" src="main.js"></script> <!-- User Interface --> <script type="text/JavaScript" src="ui.js"></script> <!-- File Menu --> <script type="text/JavaScript" src="file.js"></script> <!-- Tools --> <script type="text/JavaScript" src="tools.js"></script> <!-- Layer Transformations --> <script type="text/JavaScript" src="layer.js"></script> <!-- Image Transformations --> <script type="text/JavaScript" src="image.js"></script> <!-- Custom Filters --> <script type="text/JavaScript" src="ConvolutionFilter.js"></script> <script type="text/JavaScript" src="filters.js"></script> <!-- Simple Scripting System --> <script type="text/JavaScript" src="scripts.js"></script> </head> <body> <!-- Shade for all Dialogs --> <div id="overlay"></div> <!-- Crop Layer Element --> <div id="cropoverlay" class="dialog"> <div></div> <button style="position: absolute; top: -33px;" class="button-ok">Crop</button><button style="position: absolute; top: -33px; left: 50px;" class="button-cancel">Cancel</button> </div> <!-- Various Dialogs --> <div id="dialog-openurl" class="dialog"> Please enter url to open:<br> <input type="text" style="width: 350px;"/> <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-scale" class="dialog"> Set scale:<br> X: <input class="input-scaleX" type="text" style="width: 50px; text-align: right;" value="100"/>% Y: <input class="input-scaleY" type="text" style="width: 50px; text-align: right;" value="100"/>% <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-rotate" class="dialog"> Rotate:<br> <input type="text" style="width: 50px; text-align: right;" value="0"/>° <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-skew" class="dialog"> Skew:<br> X: <input class="input-skewX" type="text" style="width: 50px; text-align: right;" value="100"/>° Y: <input class="input-skewY" type="text" style="width: 50px; text-align: right;" value="100"/>° <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-layerrename" class="dialog"> Rename layer:<br> <input type="text" style="width: 350px;"/> <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-tooltext" class="dialog"> Add text layer:<br> Font: <select> <option value="Calibri">Calibri</option> <option value="Times New Roman">Times New Roman</option> <option value="Courier New">Courier New</option> </select> Size: <input type="text" class="input-size" style="width: 50px" value="12px"/> Color: <input type="text" class="input-color" style="width: 70px; background: black; color: silver;" value="black"/><br> <input type="text" class="input-text" style="width: 318px"/> <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-filterbrightness" class="dialog"> Set brightness:<br> <input type="text" style="width: 50px;"/>% <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-filterblur" class="dialog"> Blur radius:<br> <input type="text" style="width: 50px;"/>px <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-filtercolorify" class="dialog"> Colorify:<br> R: <input class="r" type="text" style="width: 30px;"/> G: <input class="g" type="text" style="width: 30px;"/> B: <input class="b" type="text" style="width: 30px;"/> A: <input class="a" type="text" style="width: 30px;"/> <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-filtergaussianblur" class="dialog"> Blur radius:<br> <input type="radio" class="7" name="radius"/> 3px <input type="radio" class="5" name="radius"/> 2px <input type="radio" class="3" name="radius"/> 1px <button class="button-ok">Ok</button><button class="button-cancel">Cancel</button> </div> <div id="dialog-executescript" class="dialog"> Execute script:<br> <textarea style="width: 350px; height: 200px;"></textarea><br> <button class="button-ok">Execute</button><button class="button-cancel">Cancel</button> </div> <!-- Main Menu --> <ul id="mainmenu"> <li> <button>File</button> <ul class="submenu"> <li><button id="button-openfile"><input type="file"/><span style="margin-top: -32px; display: block;">Open File</span></button></li> <li><button id="button-openurl">Open URL</button></li> <li><hr/></li> <li><button id="button-importfile"><input type="file" multiple="true"/><span style="margin-top: -32px; display: block;">Import File</span></button></li> <li><button id="button-importurl">Import URL</button></li> <li><hr/></li> <li><button id="button-save">Save</button></li> <li><button id="button-print">Print</button></li> </ul> </li> <li> <button>Edit</button> <ul class="submenu"> <li><button id="button-undo">Undo</button></li> <li><button id="button-redo">Redo</button></li> </ul> </li> <li> <button>Layer</button> <ul class="submenu"> <li><button id="button-layercrop">Crop</button></li> <li><button id="button-layerscale">Scale</button></li> <li><hr/></li> <li><button id="button-layerrotate">Rotate</button></li> <li><button id="button-layerskew">Skew</button></li> <li><button id="button-layerflipv">Flip Vertically</button></li> <li><button id="button-layerfliph">Flip Horizontally</button></li> </ul> </li> <li> <button>Image</button> <ul class="submenu"> <li><button id="button-imagescale">Scale</button></li> <li><hr/></li> <li><button id="button-imagerotate">Rotate</button></li> <li><button id="button-imageskew">Skew</button></li> <li><button id="button-imageflipv">Flip Vertically</button></li> <li><button id="button-imagefliph">Flip Horizontally</button></li> </ul> </li> <li> <button>Filters</button> <ul class="submenu"> <li><button id="button-filterbrightness">Brigtness</button></li> <li><button id="button-filtercolorify">Colorify</button></li> <li><button id="button-filterdesaturation">Desaturation</button></li> <li><hr/></li> <li><button id="button-filterblur">Blur</button></li> <li><button id="button-filtergaussianblur">Gaussian Blur</button></li> <li><button id="button-filteredgedetection">Edge Detection</button></li> <li><button id="button-filteredgeenhance">Edge Enhance</button></li> <li><button id="button-filteremboss">Emboss</button></li> <li><button id="button-filtersharpen">Sharpen</button></li> </ul> </li> <li> <button id="button-executescript">Execute Script</button> </li> <li style="float: right;"><button id="button-text"/></li> <li style="float: right;"><button id="button-move"/></li> <li style="float: right;"><button id="button-select" class="active"/></li> </ul> <!-- Right Layer Panel --> <ul id="layers"></ul> <!-- Canvas for Drawing --> <canvas/> </body> </html>
Perhatikan spesifikasi doctype HTML5. Tidak ada gunanya, panjang DTD spesifikasi; hanya kata html.
Di atas, kami link perpustakaan diperlukan dan JS file. Ini semua termasuk dalam download sumber; Perpustakaan Khusus yang EaselJS, jQuery, dan jQuery UI.
(Untuk pengantar EaselJS, lihat tutorial ini.)
Selanjutnya kita membangun seluruh struktur UI. Hanya ingat: setiap div dengan kelas dialog hanyalah sebuah dialog bagi pengguna untuk memasukkan data yang dibutuhkan untuk melakukan beberapa operasi pada gambar. Jika Anda menjalankan kode ini di browser Anda sekarang, Anda akan melihat beberapa kesalahan 404 di konsol dan bahwa menu itu tidak akan berfungsi, tetapi kami akan memperbaikinya ketika kami membuat file ui.js.
Langkah 3: Objek Aplikasi Utama
Ini adalah praktik yang baik untuk membungkus semua aplikasi Anda terkait fungsi dan variabel di dalam satu objek, untuk mencegah mereka dari overriden oleh pustaka eksternal atau bahkan skrip Anda sendiri. Objek kami akan terlihat seperti ini:
app = { stage: null, canvas: null, layers: [], tool: TOOL_SELECT, callbacks: {}, selection: { x: -1, y: -1 }, renameLayer: 0, undoBuffer: [], redoBuffer: [] }
(Saya memulai semua nama variabel dan fungsi dengan huruf kecil berdasarkan pada Konvensi Kode Douglas Crockford untuk Bahasa Pemrograman JavaScript.)
Di app.stage kita akan mengadakan referensi ke objek Stage untuk aplikasi kita. Jika Anda telah mengkodekan apa pun di ActionScript, pikirkan Panggung ini seperti AS3. Ini memiliki daftar tampilan yang ditarik ke elemen kanvas pada setiap pembaruan. App.canvas bervariasi dengan mengacu pada elemen kanvas di dalam dokumen html kami. Kami akan menggunakannya untuk membuat Panggung dan untuk mengubah ukurannya bersama dengan jendela.
Array app.layers akan menampung semua layer gambar, dan app.tool berisi nilai alat yang sebenarnya dipilih. App.callbacks akan menahan setiap panggilan balik acara yang perlu kami tentukan (misalnya mengklik tombol menu), app.renameLayer memegang nomor dari lapisan yang benar-benar diganti namanya, dan app.undoBuffer dan app.redoBuffer adalah larik untuk menahan aplikasi yang dicadangkan. lapisan negara untuk membuat fungsi undo dan redo berfungsi.
Anda juga perlu menambahkan empat baris ini sebelum definisi app
(mereka hanya konstanta ID alat):
const TOOL_MOVE = 0, TOOL_SELECT = 1, TOOL_TEXT = 2;
Langkah 4: Metode Berguna
Sekarang, kita akan mendefinisikan metode objek ini. Pertama tambahkan metode refreshLayers () dan sortLayers () berikut:
refreshLayers: function () { if ((this.getActiveLayer() == undefined) && (this.layers.length > 0)) this.layers[0].active = true; this.stage = new Stage(this.canvas); this.stage.regX = -this.canvas.width / 2; this.stage.regY = -this.canvas.height / 2; app.layers.toString = function () { var ret = []; for (var i = 0, layer; layer = this[i]; i++) { ret.push('{"x":' + layer.x + ',"y":' + layer.y + ',"scaleX":' + layer.scaleX + ',"scaleY":' + layer.scaleY + ',"skewX":' + layer.skewX + ',"skewY":' + layer.skewY + ',"active":' + layer.active + ',"visible":' + layer.visible + ',"filters":{"names":[' + (layer.filters != null ? layer.filters.toString().replace(/(\[|\])/g, '"'): 'null') + '],"values":[' + JSON.stringify(layer.filters) + ']}}'); } return '[' + ret.join(',') + ']'; } $('ul#layers').html(''); for (var i = 0, layer; layer = this.layers[i]; i++) { var self = this; self.stage.addChild(layer); (function(t, n) { layer.onClick = function (e) { if ((self.tool != TOOL_TEXT) || (!t.text)) return true; self.activateLayer(t); editText = true; } layer.onPress = function (e1) { if (self.tool == TOOL_SELECT) { self.activateLayer(t); } var offset = { x: t.x - e1.stageX, y: t.y - e1.stageY } if (self.tool == TOOL_MOVE) self.addUndo(); e1.onMouseMove = function (e2) { if (self.tool == TOOL_MOVE) { t.x = offset.x + e2.stageX; t.y = offset.y + e2.stageY; } } }; })(layer, i); layer.width = (layer.text != null ? layer.getMeasuredWidth() * layer.scaleX: layer.image.width * layer.scaleX); layer.height = (layer.text != null ? layer.getMeasuredLineHeight() * layer.scaleY: layer.image.height * layer.scaleY); layer.regX = layer.width / 2; layer.regY = layer.height / 2; $('ul#layers').prepend('<li id="layer-' + i + '" class="' + (layer.active ? 'active': '') + '"><img src="' + (layer.text != undefined ? '': layer.image.src) + '"/><h1>' + ((layer.name != null) && (layer.name != '') ? layer.name: 'Unnamed layer') + '</h1><span><button class="button-delete">Delete</button><button class="button-hide">' + (layer.visible ? 'Hide': 'Show') + '</button><button class="button-rename">Rename</button></span></li>'); } this.stage.update(); $('ul#layers').sortable({ stop: function () { app.sortLayers(); } }); if (this.layers.length > 0) { $('#button-layercrop').attr('disabled', false); $('#button-layerscale').attr('disabled', false); $('#button-layerrotate').attr('disabled', false); $('#button-layerskew').attr('disabled', false); $('#button-layerflipv').attr('disabled', false); $('#button-layerfliph').attr('disabled', false); $('#button-imagescale').attr('disabled', false); $('#button-imagerotate').attr('disabled', false); $('#button-imageskew').attr('disabled', false); $('#button-imageflipv').attr('disabled', false); $('#button-imagefliph').attr('disabled', false); $('#button-filterbrightness').attr('disabled', false); $('#button-filtercolorify').attr('disabled', false); $('#button-filterdesaturation').attr('disabled', false); $('#button-filterblur').attr('disabled', false); $('#button-filtergaussianblur').attr('disabled', false); $('#button-filteredgedetection').attr('disabled', false); $('#button-filteredgeenhance').attr('disabled', false); $('#button-filteremboss').attr('disabled', false); $('#button-filtersharpen').attr('disabled', false); } else { $('#button-layercrop').attr('disabled', true); $('#button-layerscale').attr('disabled', true); $('#button-layerrotate').attr('disabled', true); $('#button-layerskew').attr('disabled', true); $('#button-layerflipv').attr('disabled', true); $('#button-layerfliph').attr('disabled', true); $('#button-imagescale').attr('disabled', true); $('#button-imagerotate').attr('disabled', true); $('#button-imageskew').attr('disabled', true); $('#button-imageflipv').attr('disabled', true); $('#button-imagefliph').attr('disabled', true); $('#button-filterbrightness').attr('disabled', true); $('#button-filtercolorify').attr('disabled', true); $('#button-filterdesaturation').attr('disabled', true); $('#button-filterblur').attr('disabled', true); $('#button-filtergaussianblur').attr('disabled', true); $('#button-filteredgedetection').attr('disabled', true); $('#button-filteredgeenhance').attr('disabled', true); $('#button-filteremboss').attr('disabled', true); $('#button-filtersharpen').attr('disabled', true); } }, sortLayers: function () { var tempLayers = [], layersList = $('ul#layers li'); for (var i = 0, layer; layer = $(layersList[i]); i++) { if (layer.attr('id') == undefined) break; tempLayers[i] = this.layers[layer.attr('id').replace('layer-', '') * 1]; } tempLayers.reverse(); this.layers = tempLayers; this.refreshLayers(); }
Harap dicatat bahwa di dalam objek Anda perlu mendeklarasikan variabel dan fungsi dengan ':' bukannya '='.
Metode sortLayers()
dipanggil ketika pengguna menyeret lapisan dalam panel Lapisan; refreshLayers()
disebut sangat sering, karena menciptakan ulang app.stage
dan menambahkan semua layer ke panggung sekali lagi, mengatur properti mereka dan menerapkan callback peristiwa. Callback ini memungkinkan Anda untuk memindahkan lapisan dan mengedit teks pada lapisan teks. Ini adalah fungsi yang sangat penting karena juga menambahkan semua lapisan ke panel Lapisan di UI dan menonaktifkan tombol alat di menu (jika tidak ada lapisan) dan memungkinkan mereka juga (bila ada setidaknya satu lapisan).
Sebelum refreshLayers (), masukkan fungsi helper lainnya (ingat untuk menambahkan koma setelah yang terakhir!):
getActiveLayer: function () { var ret; this.layers.forEach(function(v) { if (v.active) ret = v; }); if ((ret == undefined) && (this.layers.length > 0)) return this.layers[0]; return ret; }, getActiveLayerN: function () { for (var i = 0, layer; layer = this.layers[i]; i++) { if (layer.active) return i; } }, activateLayer: function (layer) { this.layers.forEach(function (v) { v.active = false; }); if (layer instanceof Bitmap) { layer.active = true; } else { if (this.layers[layer] == undefined) return; this.layers[layer].active = true; } this.refreshLayers(); },
Lapisan aktif adalah yang mana Anda menerapkan semua operasi (tranformations, menambahkan filter, dll.). Anda dapat mengaktifkan lapisan dengan mengkliknya pada panel Lapisan atau menggunakan alat Pilih di kanvas.
Seperti yang Anda lihat parameter parameter activateLayer () dapat berupa Bitmap atau angka. Jika itu adalah Bitmap - EaselJS objek untuk gambar - maka aktif properti diatur ke true, dan jika beberapa maka lapisan pada posisi ini dalam app.layers array diaktifkan. getActiveLayer() hanya kembali lapisan yang aktif dan getActiveLayerN() kembali posisi lapisan aktif dalam app.layers array.
Sekelompok terakhir metode di objek ini harus insterted langsung setelah Deklarasi app.redoBuffer dan sebelum orang-orang yang Anda menempatkan di sana sebelumnya:
addUndo: function () { this.undoBuffer.push(this.layers.toString()); this.redoBuffer = []; }, loadLayers: function (from, to) { var json, jsonString = from.pop(); if (jsonString == undefined) return false; to.push(this.layers.toString()); json = JSON.parse(jsonString); for (var i = 0, layer, jsonLayer; ((layer = this.layers[i]) && (jsonLayer = json[i])); i++) { for (value in jsonLayer) { if (value != 'filters') { layer[value] = jsonLayer[value]; } else { var hadFilters = (layer.filters != null && layer.filters.length > 0); layer.filters = []; for (var j = 0; j < jsonLayer.filters.names.length; j++) { if (jsonLayer.filters.names[j] == null) break; layer.filters[j] = new window[jsonLayer.filters.names[j]]; for (value2 in jsonLayer.filters.values[0][j]) { layer.filters[j][value2] = jsonLayer.filters.values[0][j][value2]; } hadFilters = true; } if (hadFilters) { if (layer.cacheCanvas) { layer.updateCache(); } else { layer.cache(0, 0, layer.width, layer.height); } } } } } this.refreshLayers(); }, undo: function () { this.loadLayers(this.undoBuffer, this.redoBuffer); }, redo: function () { this.loadLayers(this.redoBuffer, this.undoBuffer); },
Seperti yang Anda perhatikan ketika membaca app.refreshLayers (), toString () metode app.layers dikesampingkan oleh kode yang menyiapkan versi stringified dari semua lapisan di dalamnya. Tentu itu akan membuang-buang memori untuk menyimpan semua informasi lapisan di sana, jadi hanya nilai-nilai yang dapat diubah dengan aplikasi yang didukung.
Metode addUndo() mendorong keadaan sebenarnya lapisan ke app.undoBuffer array dan membersihkan app.redoBuffer - karena ketika Anda melakukan tindakan yang dapat dibatalkan maka Anda tidak dapat mengulang apa pun yang dibatalkan sebelum tindakan itu. loadLayers() mengambil dua argumen (array yang kita harus pop negara app.layers dan array yang kita harus mendorong keadaan yang sebenarnya variabel ini), dan melakukan parsing terhadap app.layers didukung.
Sebagai EaselJS's filter contoh mengatakan:
"... filter hanya ditampilkan ketika tampilan objek cache..."
(Dari contoh-contoh EaselJS: filter.)
Ini berarti bahwa Anda perlu untuk memanggil method cache() Bitmap untuk menerapkan filter. Caching dilakukan untuk meningkatkan kinerja - filter diterapkan hanya sekali dan hanya bitmap disaring ditarik. EaselJS adalah caching konten dalam cara yang sangat pintar - itu hanya menyalinnya ke kanvas elemen lain yang tidak ditambahkan ke dokumen (tersembunyi). Saya menyebutkan hal ini karena pada akhir loadLayers() metode ada jika blok yang memeriksa apakah ada penyaring apapun yang harus diupdate pada lapisan ini - dan jika ada, pembaruan cache atau cache elemen.
Langkah 5: inisialisasi
Inisialisasi aplikasi yang utuh sederhana; hanya masukkan ini setelah Deklarasi app:
tick = function () { app.stage.update(); } $(document).ready(function () { app.canvas = $('canvas')[0]; document.onselectstart = function () { return false; }; Ticker.setFPS(30); Ticker.addListener(window); });
Ticker ini dilaksanakan EaselJS timer yang memanggil fungsi tick() pendengar untuk mempertahankan FPS stabil yang ditetapkan sebelumnya. Dengan cara ini kita dapat secara otomatis memanggil app.stage.update() untuk redraw panggung.
Pada awal (setelah dokumen telah dimuat) kami menetapkan kanvas elemen pertama pada halaman yang menemukan $ (jQuery) fungsi untuk app.canvas, maka kami menonaktifkan memilih apa dokumen (karena sebaliknya ketika Anda tarik mouse di kanvas ada Efek seperti jika Anda memilih teks).
Kami menetapkan Ticker di FPS-30 (Anda perlu hanya 24 frame per detik untuk menipu mata manusia berpikir itu adalah melihat gerakan) dan mengatur jendela sebagai pendengar Ticker.
Langkah 6: UI penolong fungsi
Sekarang saatnya untuk membawa menu kami dan seluruh user interface untuk hidup. Ui.js file akan terdiri hampir seluruhnya dari fungsi jQuery, sehingga benar-benar mudah untuk memahami. Mari kita mulai dengan fungsi pembantu:
importFile = false; hideDialog = function (dialog) { $(dialog).hide(); if ($('.dialog:visible').length == 0) $('#overlay').hide(); editText = false; } showDialog = function (dialog) { $('#overlay').show(); $(dialog).show(); }
Variabel importFile akan menginformasikan kepada kami tentang apakah kita membuka sebuah file atau mengimpor.
Nama-nama showDialog() dan hideDialog() fungsi berbicara sendiri - meskipun satu hal interesing dalam fungsi hideDialog() adalah bagaimana memeriksa apakah semua dialog yang tersembunyi dengan jQuery ': terlihat ' kelas pseudo, hanya untuk kemudian menyembunyikan hamparan. Pada akhirnya itu terbukti menjadi tidak berguna karena tidak ada situasi bahwa lebih dari satu dialog di layar, tapi aku meninggalkannya untuk penggunaan masa depan Anda; mungkin itu akan datang berguna.
Langkah 7: Mengubah ukuran panggung
Sekarang kita harus melakukan sesuatu ketika pengguna mengubah ukuran jendela browser. Ini adalah ketika peristiwa yang mengubah ukuran jendela datang ke dalam bermain. Dipecat setiap kali pengguna mengubah ukuran jendela browser:
$(window).resize(function () { $('.dialog').each(function () { $(this).css({ left: window.innerWidth / 2 - $(this).outerWidth() / 2 + 'px', top: window.innerHeight / 2 - $(this).outerHeight() / 2 + 'px' }); }); $('canvas').attr('height', $(window).height() - 37).attr('width', $(window).width() - 232); $('ul#mainmenu').width($(window).width() - 4); $('ul#layers').css({ height: $(window).height() - 37 }); app.refreshLayers(); if ($('#cropoverlay').css('display') == 'block') { $('#cropoverlay').css({ left: Math.ceil(app.canvas.width / 2 - app.getActiveLayer().x - app.getActiveLayer().regX - 1) + 'px', top: Math.ceil(app.canvas.height / 2 + app.getActiveLayer().y - app.getActiveLayer().regY + 38) + 'px' }); } });
Pertama kami pusat semua dialog menggunakan jQuery's each() fungsi. Ini panggilan callback untuk setiap item yang cocok dengan pemilih dalam fungsi $.
Kemudian, kita harus menetapkan kanvas lebar dan tinggi - tetapi tidak dalam CSS karena ini akan strech gambar dalam kanvas, dan kita tidak ingin yang. Ketinggian menu adalah 37px sehingga kita menetapkan kanvas tinggi jendela tinggi minus 37px. Sama untuk lebar, tapi kali ini kami punya untuk mengurangi lebar panel lapisan yang 232px. Kami juga Resize menu dan panel lapisan untuk menyesuaikan jendela (di sini kita dapat menggunakan CSS).
Setelah itu kita perlu menyegarkan lapisan untuk memastikan mereka selalu up-to-date ketika jendela diubah ukurannya. Hal terakhir adalah untuk memindahkan dialog tanaman dalam kasus pengguna diubah ukurannya jendela ketika tanam lapisan.
Langkah 8: Mengikat semua bersama-sama
Tombol menu's harus diikat ke callback yang ditentukan dalam app.callbacks, dan juga kita perlu untuk mengikat keydown event untuk input dan klik untuk tombol dialog. Kalimat terakhir mungkin kedengarannya rumit, tetapi ketika Anda melihat kode ini akan menjadi jelas:
$(document).ready(function () { $("ul#mainmenu li button").click(function () { $(this).focus(); $(this).parent().find("ul.submenu:visible").slideUp('fast').show(); $(this).parent().find("ul.submenu:hidden").slideDown('fast').show(); }); $("ul#mainmenu li button").blur(function () { $(this).parent().find("ul.submenu:visible").delay(100).slideUp('fast').show(); }); $('#button-openfile').hover( function () { $(this).addClass('hover'); }, function () { $(this).removeClass('hover'); } ); $('#button-importfile').hover( function () { $(this).addClass('hover'); }, function () { $(this).removeClass('hover'); } ); $('#button-openurl').click(function () { importFile = false; showDialog('#dialog-openurl'); $('#dialog-openurl input').val('').attr('disabled', false).focus(); }); $('#button-importurl').click(function () { importFile = true; showDialog('#dialog-openurl'); $('#dialog-openurl input').val('').attr('disabled', false).focus(); }); $('#button-undo').click(function () { app.undo(); }); $('#button-redo').click(function () { app.redo(); }); $('#button-layerscale').click(function () { affectImage = false; showDialog('#dialog-scale'); $('#dialog-scale input.input-scaleX').val('100'); $('#dialog-scale input.input-scaleY').val('100'); }); $('#button-layerskew').click(function () { affectImage = false; showDialog('#dialog-skew'); $('#dialog-skew input.input-scaleX').val('100'); $('#dialog-skew input.input-scaleY').val('100'); }); $('#button-layerrotate').click(function () { affectImage = false; showDialog('#dialog-rotate'); $('#dialog-rotate input').val('0'); }); $('#button-layercrop').click(function () { affectImage = false; app.sortLayers(); app.refreshLayers(); var layer = app.getActiveLayer(); $('#overlay').show(); $('#cropoverlay').css({ left: Math.ceil(app.canvas.width / 2 + layer.x - layer.regX - 1) + 'px', top: Math.ceil(app.canvas.height / 2 + layer.y - layer.regY + 38) + 'px', width: (layer.text != null ? layer.getMeasuredWidth() * layer.scaleX: layer.image.width * layer.scaleX) + 2 + 'px', height: (layer.text != null ? layer.getMeasuredLineHeight() * layer.scaleY: layer.image.height * layer.scaleY) + 2 + 'px' }).show(); }); $('#button-layerflipv').click(app.callbacks.layerFlipV); $('#button-layerfliph').click(app.callbacks.layerFlipH); $('#button-imagescale').click(function () { affectImage = true; showDialog('#dialog-scale'); $('#dialog-scale input.input-scaleX').val('100'); $('#dialog-scale input.input-scaleY').val('100'); }); $('#button-imageskew').click(function () { affectImage = true; showDialog('#dialog-skew'); $('#dialog-skew input.input-scaleX').val('100'); $('#dialog-skew input.input-scaleY').val('100'); }); $('#button-imagerotate').click(function () { affectImage = true; showDialog('#dialog-rotate'); $('#dialog-rotate input').val('0'); }); $('#button-imageskew').click(function () { affectImage = true; showDialog('#dialog-skew'); $('#dialog-skew input.input-skewX').val('0'); $('#dialog-skew input.input-skewY').val('0'); }); $('#button-filterbrightness').click(function () { showDialog('#dialog-filterbrightness'); $('#dialog-filterbrightness input').val('100'); }); $('#button-filtercolorify').click(function () { showDialog('#dialog-filtercolorify'); $('#dialog-filtercolorify input').val('0'); }); $('#button-filterblur').click(function () { showDialog('#dialog-filterblur'); $('#dialog-filterblur input').val('1'); }); $('#button-filtergaussianblur').click(function () { showDialog('#dialog-filtergaussianblur'); $('#dialog-filtergaussianblur input.7').attr('checked', true); }); $('#button-executescript').click(function () { showDialog('#dialog-executescript'); $('#dialog-executescript textarea').val(''); }); $('#button-select').click(function () { app.tool = TOOL_SELECT; $('#mainmenu button').removeClass('active'); $(this).addClass('active'); }); $('#button-move').click(function () { app.tool = TOOL_MOVE; $('#mainmenu button').removeClass('active'); $(this).addClass('active'); }); $('#button-text').click(function () { app.tool = TOOL_TEXT; $('#mainmenu button').removeClass('active'); $(this).addClass('active'); }); $('#button-imageflipv').click(app.callbacks.imageFlipV); $('#button-imagefliph').click(app.callbacks.imageFlipH); $('#dialog-openurl input').keydown(app.callbacks.openURL); $('#dialog-openurl button.button-ok').click(app.callbacks.openURL); $('#dialog-scale input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerScale); $('#dialog-scale button.button-ok').click(app.callbacks.layerScale); $('#button-openfile input').change(app.callbacks.openFile); $('#button-importfile input').change(app.callbacks.importFile); $('#dialog-tooltext button.button-ok').click(app.callbacks.toolText); $('#dialog-tooltext input').keydown(app.callbacks.toolText); $('#dialog-layerrename button.button-ok').click(app.callbacks.layerRename); $('#dialog-layerrename input').keydown(app.callbacks.layerRename); $('#dialog-rotate button.button-ok').click(app.callbacks.layerRotate); $('#dialog-rotate input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerRotate); $('#dialog-skew button.button-ok').click(app.callbacks.layerSkew); $('#dialog-skew input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerSkew); $('#cropoverlay button.button-ok').click(app.callbacks.layerCrop); $('#button-filterdesaturation').click(app.callbacks.filterDesaturation); $('#button-filteredgedetection').click(app.callbacks.filterEdgeDetection); $('#button-filteredgeenhance').click(app.callbacks.filterEdgeEnhance); $('#button-filteremboss').click(app.callbacks.filterEmboss); $('#button-filtersharpen').click(app.callbacks.filterSharpen); $('#dialog-filterbrightness button.button-ok').click(app.callbacks.filterBrightness); $('#dialog-filterbrightness input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterBrightness); $('#dialog-filtergaussianblur button.button-ok').click(app.callbacks.filterGaussianBlur); $('#dialog-filtergaussianblur input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterGaussianBlur); $('#dialog-filterblur button.button-ok').click(app.callbacks.filterBlur); $('#dialog-filterblur input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterBlur); $('#dialog-filtercolorify button.button-ok').click(app.callbacks.filterColorify); $('#dialog-filtercolorify input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterColorify); $('#dialog-executescript button.button-ok').click(app.callbacks.scriptExecute); $('#button-save').click(app.callbacks.saveFile); $('#button-print').click(app.callbacks.printFile); $('#dialog-tooltext input.input-color').keyup(function (e) { $(this).css({ backgroundColor: $(this).val() }); }); $('ul#layers li').live('click', function () { app.activateLayer($(this).attr('id').replace('layer-', '') * 1); }); $('ul#layers li button.button-delete').live('click', function () { app.layers.splice($(this).parent().parent().attr('id').replace('layer-', '') * 1, 1); this.undoBuffer = []; this.redoBuffer = []; app.refreshLayers(); }); $('ul#layers li button.button-hide').live('click', function () { if ($(this).text() == 'Hide') { app.layers[$(this).parent().parent().attr('id').replace('layer-', '') * 1].visible = false; } else { app.layers[$(this).parent().parent().attr('id').replace('layer-', '') * 1].visible = true; } app.refreshLayers(); }); $('ul#layers li button.button-rename').live('click', function () { $('#dialog-layerrename').show(); $('#overlay').show(); $('#dialog-layerrename input').val(''); app.renameLayer = $(this).parent().parent().attr('id').replace('layer-', '') * 1; }); $(document).keydown(function (e) { if (e.keyCode == 27) { hideDialog('.dialog'); } }); $('.dialog button.button-cancel').each(function () { $(this).click(function () { hideDialog($(this).parent()); }); }); $('canvas').click(app.callbacks.toolText); $('#cropoverlay').draggable().resizable({ handles: 'se', resize: function (e, ui) { $('#cropoverlay').css({ left: ui.position.left + 'px', top: ui.position.top + 'px' }); }, stop: function (e, ui) { $('#cropoverlay').css({ left: ui.position.left + 'px', top: ui.position.top + 'px' }); } }); $(window).resize(); });
Satu hal yang perlu diingat: ketika Anda melakukan apa-apa dengan jQuery dan setiap elemen HTML, melakukan itu di dalam document.ready callback, karena hanya maka Anda dapat yakin bahwa semua elemen, Anda menggunakan yang sudah diberikan.
Kode di atas lama, tetapi karena ada banyak bagian yang sama tetapi berbeda dalam bagian-bagian yang tidak memungkinkan kita untuk bungkus dalam setiap fungsi pembantu. Bagian yang disorot adalah tempat kami mengatur semua callback untuk tombol dan input dialog.
Selanjutnya Anda harus melihat pada fungsi live () yang kita gunakan untuk mengikat event klik ke tombol layer (pada panel Layer) - fungsi live () menambahkan callback untuk setiap elemen yang akan cocok dengan pemilih ini di masa depan, membuatnya sangat berguna karena kami membuat daftar layer baru di setiap app.refreshLayers () panggilan.
Fungsi terakhir di sini adalah $ (window) .resize () yang secara manual memunculkan peristiwa pengubahan ukuran jendela. Inilah sebabnya mengapa penting untuk menghubungkan skrip secara berurutan, karena jika ui.js ditambahkan ke HTML sebelum main.js, layer akan di-refresh sebelum definisi fungsi, yang terkadang dapat menyebabkan hasil yang tidak diharapkan, sehingga menemukan bug bahkan lebih keras.
Jika Anda menjalankan aplikasi sekarang, Anda akan melihat menu yang berfungsi baik dan mengubah ukuran UI dengan tepat, tetapi tetap tidak ada tombol yang akan melakukan apa pun selain melemparkan kesalahan ke konsol saat Anda mengekliknya.
Langkah 9: Membuka File
Sekarang kita akan menggunakan API lain dari spesifikasi HTML5: File API. Ini memungkinkan kita untuk membuka file dari komputer pengguna, tetapi hanya ketika ia memilihnya di kolom input file OS (untuk mencegah aplikasi web mencuri data pribadi Anda).
Harap perhatikan bahwa jika Anda akan menjalankan aplikasi ini di komputer lokal, Anda perlu mengatur server lokal atau menambahkan parameter --allow-file-access-from-files ketika menjalankan Chrome, karena membuka file dari dalam halaman web lokal dinonaktifkan oleh default.
Di file file.js kita juga akan meletakkan fungsi untuk menyimpan dan mencetak gambar, jadi mari kita mulai dengan keempat pembantu ini:
openFile = function (url, first) { var img = new Image(); img.onload = function () { var n = (first ? 0: app.layers.length); if (first) app.layers = []; app.layers[n] = new Bitmap(img); app.layers[n].x = 0; app.layers[n].y = 0; app.activateLayer(n); } img.src = url; this.undoBuffer = []; this.redoBuffer = []; } openURL = function (self, url) { $(self).attr('disabled', true); openFile(url, !importFile); hideDialog('#dialog-openurl'); } saveFile = function () { window.open(app.stage.toDataURL()); } printFile = function () { window.print() }
Fungsi openFile akan digunakan untuk membuka gambar dan menambahkannya ke layer. Jika kita memilih 'Buka File' dari menu maka konten lama akan dihapus, sedangkan 'Impor File' akan menambahkan layer baru ke gambar. (Dalam fungsi, jika parameter pertama benar maka kita membuka file, jika tidak kita mengimpornya).
openURL membuka file menggunakan fungsi yang sama, tetapi dari sumber eksternal (dan sejauh yang saya tahu Chrome menonaktifkan akses ke data pixel asal lintas domain yang membuat gambar mereka cukup berguna). Karena kami tidak dapat menyimpan file ke disk pengguna kami hanya membuka jendela lain yang mengandung hanya gambar yang mewakili tahap yang sebenarnya; pengguna dapat kemudian klik kanan untuk menyimpan gambar.
Percetakan dicapai dengan memanggil window.print(). Kita tidak bisa, tentu saja, mencetak apa pun tanpa membiarkan pengguna mengetahui, sehingga fungsi ini akan membuka dialog cetak default dimana pengguna dapat memilih preferensi pencetakan.
Sekarang kita akan menentukan beberapa callback untuk tombol dalam menu:
app.callbacks.openFile = function (e) { var file = e.target.files[0], self = this; if (!file.type.match('image.*')) return false; var reader = new FileReader(); reader.onload = function(e) { openFile(e.target.result, true); }; reader.readAsDataURL(file); }; app.callbacks.openURL = function (e) { switch (e.type) { case "click": openURL($('#dialog-openurl input'), $('#dialog-openurl input').val()); break; case "keydown": if (e.keyCode == 13) openURL(this, $(this).val()); break; } } app.callbacks.importFile = function (e) { for (var i = 0, file; file = e.target.files[i]; i++) { if (!file.type.match('image.*')) continue; var reader = new FileReader(); reader.onload = function(e) { openFile(e.target.result, false); }; reader.readAsDataURL(file); } }; app.callbacks.saveFile = function () { saveFile(); } app.callbacks.printFile = function () { printFile(); }
Seperti Anda dapat melihat kode sangat singkat namun sangat kuat. Pertama (app.callbacks.openFile) callback kita memeriksa apakah dibuka file gambar, dan berhenti jika tidak. Kemudian kita menciptakan FileReader baru, menetapkan yang onload callback untuk membuka file, dan memanggil metode readAsDataURL(file) yang load file dan output hasil sebagai URL data untuk kita untuk membaca.
(Juga perlu diketahui bahwa kami membersihkan undo dan mengulang array; kita harus melakukan ini karena kami tidak dapat mengembalikan gambar jika kami menghapus itu - pengguna harus secara manual memilih ulang file dari input.)
Simpan file, membuka app di browser Anda, dan Anda dapat akhirnya melakukan sesuatu! Tidak banyak, tetapi jika Anda melakukan ini untuk pertama kalinya mungkin menarik untuk dapat memuat beberapa gambar ke browser, bahkan jika Anda hanya dapat bergerak mereka.
Langkah 10: Teks lapisan
Sekarang bahwa Anda dapat membuka dan mengimpor gambar Anda bisa menambahkan beberapa teks. Ada satu hal yang benar-benar berguna dengan kanvas - Anda menentukan teks seperti dalam atribut font CSS. Dan EaselJS sepenuhnya menggunakan fitur itu.
Kami akan menentukan alat teks dalam tools.js file. Tambahkan baris berikut:
editText = false; toolText = function (text, font, color, size, x, y) { var n = (editText ? app.getActiveLayerN(): app.layers.length); app.layers[n] = new Text(text, size + ' ' + font, color); app.layers[n].x = x - app.canvas.width / 2; app.layers[n].y = y - app.canvas.height / 2; app.layers[n].name = text; app.activateLayer(n); hideDialog('#dialog-tooltext'); this.undoBuffer = []; this.redoBuffer = []; } app.callbacks.toolText = function (e) { switch (e.type) { case "click": if (e.target instanceof HTMLButtonElement) { toolText($('#dialog-tooltext input.input-text').val(), $('#dialog-tooltext select').val(), $('#dialog-tooltext input.input-color').val(), $('#dialog-tooltext input.input-size').val(), (editText ? app.getActiveLayer().x: app.selection.x), (editText ? app.getActiveLayer().y: app.selection.y)); } else { if (app.tool != TOOL_TEXT) return true; $('#dialog-tooltext').show(); $('#overlay').show(); app.selection.x = e.offsetX; app.selection.y = e.offsetY; $('#dialog-tooltext input.input-text').val((editText ? app.getActiveLayer().text: '')); $('#dialog-tooltext input.input-size').val((editText ? app.getActiveLayer().font.split(' ')[0]: '12px')); $('#dialog-tooltext select').val((editText ? app.getActiveLayer().font.split(' ')[1]: 'Calibri')); $('#dialog-tooltext input.input-color').val((editText ? app.getActiveLayer().color: 'black')); $('#dialog-tooltext input.input-color').css({ backgroundColor: $('#dialog-tooltext input.input-color').val() }); } break; case "keydown": if (e.keyCode == 13) toolText($('#dialog-tooltext input.input-text').val(), $('#dialog-tooltext select').val(), $('#dialog-tooltext input.input-color').val(), $('#dialog-tooltext input.input-size').val(), (editText ? app.getActiveLayer().x: app.selection.x), (editText ? app.getActiveLayer().y: app.selection.y)); break; } }
EditText variabel benar ketika kita mengubah sifat lapisan teks yang sudah ada agar tidak perlu membuat yang baru.
Fungsi pertama adalah, seperti biasa, fungsi pembantu. Ia memeriksa apakah kita mengedit lapisan teks yang ada atau menambahkan yang baru, kemudian menciptakan sebuah obyek teks baru dan menambahkannya ke lapisan aplikasi. Karena semua objek dalam EaselJS memperpanjang DisplayObject dasar, kita dapat menggunakan teks dan Bitmap dengan cara yang sama; hanya sifat mereka berbeda.
Fungsi kedua adalah callback. Hal pertama yang perlu kita lakukan adalah untuk memeriksa jenis penyelenggaraan apa kita menerima (karena kita menggunakan hanya satu callback yang baik untuk menangani klik tombol dan menekan masuk di dalam input). Kemudian kita sebut penolong sebelumnya.
Langkah 11: Lapisan transformasi
Transformasi lapisan sederhana memang sangat sederhana dengan EaselJS. Segala sesuatu yang aku akan menunjukkan di sini dapat dilakukan hanya dengan mengubah lapisan (Bitmap atau teks) properti.
Saya akan mulai dengan menjelaskan titik pendaftaran. Properti regX dan regy menentukan titik dari mana rotasi dan posisi dihitung - itu seperti pegangan. Dalam file main.js kami menetapkan titik ini ke pusat gambar, untuk membuat transformasi lapisan lebih mudah. Semua fungsi transformasi lapisan akan masuk ke file layer.js.
affectImage = false; layerScale = function (x, y) { app.addUndo(); if (affectImage) return imageScale(x, y); app.getActiveLayer().scaleX *= x / 100; app.getActiveLayer().scaleY *= y / 100; hideDialog('#dialog-scale'); } layerRotate = function (deg) { app.addUndo(); if (affectImage) return imageRotate(deg); app.getActiveLayer().rotation += deg; hideDialog('#dialog-rotate'); } layerSkew = function (degx, degy) { app.addUndo(); if (affectImage) return imageSkew(degx, degy); app.getActiveLayer().skewX += degx; app.getActiveLayer().skewY += degy; hideDialog('#dialog-skew'); } layerFlipH = function () { app.addUndo(); app.getActiveLayer().scaleX = -app.getActiveLayer().scaleX; } layerFlipV = function () { app.addUndo(); app.getActiveLayer().scaleY = -app.getActiveLayer().scaleY; }
Dalam bagian ini - seperti biasa - ada penolong fungsi. Ada beberapa hal yang saya ingin mengatakan tentang ini potongan kode.
Pertama: kita perlu memanggil fungsi addUndo() sebelum kita mulai mengubah apa-apa. Mengapa? Karena kami ingin kembali ke keadaan sebelum beberapa operasi dilakukan ketika kami mengklik tombol undo.
Juga melihat affectImage variabel. Kami akan mengaturnya untuk benar ketika kita ingin mempengaruhi seluruh gambar; dalam fungsi hampir setiap ada if pernyataan yang memeriksa jika kita mempengaruhi seluruh gambar, dan (jika demikian) kembali hasil dari fungsi sesuai image*().
Sekarang meletakkan pemanggilan kode dalam file:
app.callbacks.numberOnly = function (e) { if ((e.shiftKey) || ([8, 13, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 190, 189].indexOf(e.keyCode) < 0)) return false; } app.callbacks.layerRename = function (e) { switch (e.type) { case "click": app.layers[app.renameLayer].name = $('#dialog-layerrename input').val(); app.refreshLayers(); hideDialog('#dialog-layerrename'); break; case "keydown": if (e.keyCode == 13) { app.layers[app.renameLayer].name = $('#dialog-layerrename input').val(); app.refreshLayers(); hideDialog('#dialog-layerrename'); } break; } } app.callbacks.layerScale = function (e) { switch (e.type) { case "click": layerScale($('#dialog-scale input.input-scaleX').val() * 1, $('#dialog-scale input.input-scaleY').val() * 1); break; case "keydown": if (e.keyCode == 13) layerScale($('#dialog-scale input.input-scaleX').val() * 1, $('#dialog-scale input.input-scaleY').val() * 1); break; } } app.callbacks.layerRotate = function (e) { switch (e.type) { case "click": layerRotate($('#dialog-rotate input').val() * 1); break; case "keydown": if (e.keyCode == 13) layerRotate($(this).val() * 1); break; } } app.callbacks.layerSkew = function (e) { switch (e.type) { case "click": layerSkew($('#dialog-skew input.input-skewX').val() / 100, $('#dialog-skew input.input-skewY').val() / 100); break; case "keydown": if (e.keyCode == 13) layerSkew($('#dialog-skew input.input-skewX').val() / 100, $('#dialog-skew input.input-skewY').val() / 100); break; } } app.callbacks.layerFlipV = function () { layerFlipV(); } app.callbacks.layerFlipH = function () { layerFlipH(); } app.callbacks.layerCrop = function () { var layer = app.getActiveLayer(); layer.cache( Math.floor(app.canvas.width / 2 - $('#cropoverlay').position().left - layer.regX + layer.x - 1), Math.floor(app.canvas.height / 2 - $('#cropoverlay').position().top - layer.regY + layer.y + 38), $('#cropoverlay').width(), $('#cropoverlay').height() ); $(this).parent().find('.button-cancel').click(); }
Lain sekelompok panggilan jQuery. Kami juga memeriksa jenis acara karena kita bisa mendapatkan fungsi-fungsi yang disebut oleh tombol atau input field. Setiap fungsi dalam kode di atas adalah sama: memeriksa jenis peristiwa, mendapatkan nilai input dari dialog dan panggilan fungsi.
Callback di atas ini bagian dari kode terikat input yang hanya harus mendapatkan angka-angka di dalam mereka.
Langkah 12: Transformasi: skala
Mari kita mulai dengan menambahkan kode sumber ini; Saya akan menjelaskan itu kemudian. Letakkan kode berikut ke dalam image.js file:
imageScale = function (x, y) { for (var i = 0, layer; layer = app.layers[i]; i++) { layer.scaleX *= x / 100; layer.scaleY *= y / 100; layer.x *= x / 100; layer.y *= y / 100; } hideDialog('#dialog-scale'); affectImage = false; }
Kami hanya perulangan melalui semua lapisan pengaturan mereka scaleX dan properti scaleY. Tapi gambar akan tampak aneh jika kita hanya skala lapisan. Kita juga harus bergerak setiap lapisan untuk membuat fungsi ini bekerja dengan baik.
Langkah 13: Gambar transformasi: rotasi
Rotasi akan sedikit lebih sulit daripada scaling. Tapi pertama adalah kode etik ini; menempatkan ini juga ke dalam image.js file:
imageRotate = function (deg) { for (var i = 0, layer; layer = app.layers[i]; i++) { layer.rotation += deg; var rad = deg * Math.PI / 180, x = (layer.x * Math.cos(rad)) - (layer.y * Math.sin(rad)), y = (layer.x * Math.sin(rad)) + (layer.y * Math.cos(rad)); layer.x = x; layer.y = y; } hideDialog('#dialog-rotate'); affectImage = false; }
Kita tentu saja menambahkan lapisan rotasi, tetapi kode yang saya disorot baru. Hal ini didasarkan pada persamaan berputar titik dalam sistem koordinat Kartesius:
Mana Φ adalah sudut. Sehingga kode disorot hanya terjemahan dari persamaan di atas ke dalam kode JavaScript (ditambah konversi dari derajat ke radian, karena fungsi trigonometri Math perpustakaan mengambil radian sebagai parameter dan satu radian adalah persis pi/180 derajat).
Langkah 14: Transformasi: condong
Membuat ini sangat mirip dengan rotasi, karena pada dasarnya rotasi tapi dengan dua berbeda sudut untuk dua arah. Lihatlah kode:
imageSkew = function (degx, degy) { for (var i = 0, layer; layer = app.layers[i]; i++) { layer.skewX += degx; layer.skewY += degy; var radx = degx * Math.PI / 180, rady = degy * Math.PI / 180, x = (layer.x * Math.cos(radx)) - (layer.y * Math.sin(radx)), y = (layer.x * Math.sin(rady)) + (layer.y * Math.cos(rady)); layer.x = x; layer.y = y; } hideDialog('#dialog-skew'); affectImage = false; }
Anda melihat perbedaan? Kami hanya menggunakan radx untuk x posisi dan rady untuk posisi y. Hal ini membuat gambar condong benar (yang memberikan efek yang cukup baik, saya harus mengatakan).
Langkah 15: Transformasi: Flip
Ini adalah modifikasi dari fungsi imageScale(). Ianya tidak terakhir karena itu adalah yang paling sulit fungsi transformasi gambar, hanya karena itu pada posisi terakhir di menu. Kode:
imageFlipH = function () { app.addUndo(); for (var i = 0, layer; layer = app.layers[i]; i++) { layer.scaleX = -layer.scaleX; layer.x = -layer.x; } affectImage = false; } imageFlipV = function () { app.addUndo(); for (var i = 0, layer; layer = app.layers[i]; i++) { layer.scaleY = -layer.scaleY; layer.y = -layer.y; } affectImage = false; } app.callbacks.imageFlipV = function () { imageFlipV(); } app.callbacks.imageFlipH = function () { imageFlipH(); }
Tentu saja kita dapat mengabaikan callback - kita sudah tahu apa yang mereka lakukan. Kita harus fokus pada pertama dua fungsi. Mereka hanya perulangan melalui lapisan pengaturan mereka scaleX atau scaleY ke nilai yang sebaliknya - ini adalah apa yang kita kenal sebagai membalik: skala yang negatif. Juga x atau y harus terbalik untuk membuat gambar yang benar-benar melihat membalik.
Ini adalah transformasi gambar terakhir. Sekarang kita akan membuat sesuatu yang lebih maju - filter!
Langkah 16: Sederhana filter: Pendahuluan
Saya menyebutnya sederhana karena kami menggunakan filter yang dibangun EaselJS: ColorFilter dan ColorMatrixFilter. Ini mengubah gambar piksel dengan piksel, sehingga dengan gambar besar dan rumit filter, Anda dapat membuat browser lag untuk sementara atau bahkan menghentikan sepenuhnya.
Sebelum kita menerapkan mereka saya akan menjelaskan apa setiap filter.
ColorFilter mengambil parameter delapan penciptaan:
new ColorFilter(redMultiplier, greenMultiplier, blueMultiplier, alphaMultiplier, redOffset, greenOffset, blueOffset, alphaOffset);
Bila penyaring diterapkan membagi gambar ke empat saluran (merah, hijau, biru dan alpha) dan untuk setiap saluran mengalikan setiap nilai dengan kelipatan sesuai dan menambahkan offset sesuai. (Sebenarnya, gambar tidak benar-benar dibagi; ini adalah metafora yang berguna.)
ColorMatrixFilter mengambil satu-satunya parameter penciptaan:
new ColorMatrixFilter(matrix);
Matriks memiliki format berikut:
[ rr, rg, rb, ra, ro, gr, gg, gb, ga, go, br, bg, bb, ba, bo, ar, ag, ab, aa, ao ]
Bila penyaring ini diterapkan, itu juga (kiasan) perpecahan gambar ke dalam saluran, dan kemudian itu mengalikan setiap nilai oleh satu sama lain. Sebagai contoh, persamaan untuk nilai sebuah pixel dalam saluran merah setelah melewati filter adalah:
newRed = (red * rr) + (green * rg) + (blue * rb) + (alpha * ra) + ro;
Ini adalah sama untuk hijau, biru dan alpha juga, hanya dengan variabel yang berbeda dari matriks (gr, gg, gb, ga hijau, dan seterusnya). Filter ini sedikit lebih maju dari ColorFilter, karena masing-masing warna tergantung pada warna lainnya dari pixel.
Untuk keterangan lebih lanjut, lihat tutorial ini.
Langkah 17: Sederhana filter: fungsi pembantu
Ini adalah salah satu dua fungsi pembantu yang akan digunakan di sini, tapi kami akan menggunakannya untuk setiap filter, juga untuk yang canggih.
Letakkan kode ini di awal filters.js file:
applyFilter = function (filter) { app.addUndo(); var layer = app.getActiveLayer(); layer.filters = (layer.filters ? layer.filters: []); layer.filters.push(filter); if (layer.cacheCanvas) { layer.updateCache(); } else { layer.cache(0, 0, layer.width, layer.height); } }
Tidak semua pekerjaan untuk kita: meraih lapisan aktif; Jika tidak ada penyaring kemudian menciptakan array filter; dan menambahkan filter.
Setelah itu kita harus meng-cache layer agar efek filter terlihat, jadi kita periksa apakah kita sudah meng-cache layer ini (misalnya ketika memotongnya) dan memanggil updateCache () atau cache () sebagaimana mestinya.
Berikut gambar yang akan saya gunakan untuk menampilkan efek filter:

Langkah 18: Sederhana filter: kecerahan
Untuk efek ini kita akan menggunakan ColorFilter, karena perubahan kecerahan hanya berubah semua saluran nilai (merah, hijau, biru) dalam lapisan dengan nilai yang sama.
Ini kode (masukkan ke file filters.js):
filterBrightness = function (value) { applyFilter(new ColorFilter(value, value, value, 1)); hideDialog('#dialog-filterbrightness'); }
Seperti yang saya sebutkan sebelumnya, fungsi pembantu kami adalah melakukan segala sesuatu bagi kita, kita hanya perlu membuat penyaring baru. Di sini kami menciptakan ColorFilter dengan pengganda merah, hijau, biru yang diatur ke nilai dan alpha set 1.0 (kita tidak ingin alpha untuk disentuh oleh filter ini).
Berikut adalah contoh hasil dari filter ini:

Langkah 19: Filter Sederhana: Mewarnai
Colorify akan membuat beberapa saluran bernilai lebih besar atau lebih kecil untuk mengubah warna keseluruhan gambar, jadi kita akan kembali menggunakan ColorFilter.
Lihatlah kode:
filterColorify = function (r, g, b, a) { applyFilter(new ColorFilter(1.0, 1.0, 1.0, 1.0, r, g, b, a)); hideDialog('#dialog-filtercolorify'); }
Sekali lagi, pekerjaan kotor ditangani oleh applyFilter
, dan kami hanya fokus pada pembuatan objek filter. Di sini kita akan menggunakan empat parameter terakhir dari konstruktor ColorFilter
. Mereka ditambahkan ke saluran, sehingga mereka sangat sesuai dengan kebutuhan kita.
Di bawah ini adalah contoh hasil dari filter ini:

Langkah 20: Filter Sederhana: Desaturasi
Desaturasi adalah proses menghilangkan kejenuhan - dengan kata-kata sederhana, membuat gambar hitam dan putih. Untuk melakukan itu kita perlu menghitung luminositas setiap piksel dan mengatur semua warna ke nilai ini. Persamaan Luminositas simpliest melibatkan hanya menambahkan sama jumlah dari semua warna, dan untuk bahwa kita dapat menggunakan ColorMatrixFilter
:
filterDesaturation = function () { applyFilter(new ColorMatrixFilter( [ 0.33, 0.33, 0.33, 0.00, 0.00, 0.33, 0.33, 0.33, 0.00, 0.00, 0.33, 0.33, 0.33, 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00 ] )); hideDialog('#dialog-filterbrightness'); }
Seperti yang saya katakan sebelumnya - ambil jumlah yang sama dari tiga warna dan tambahkan. Kita lagi jangan sentuh alpha seperti itu tidak mengandung nilai warna apapun.
Tidak ada contoh gambar hasil karena sudah hitam dan putih; filter tidak berpengaruh.
Langkah 21: Lilitan filter
Filter lilitan sedikit lebih maju dari ColorMatrixFilter. Ia juga menggunakan matriks, tetapi matriks konvolusi mewakili pengganda piksel di sekitar piksel sebenarnya.
Katakanlah kita memiliki contoh matriks 3x3 konvolusi ini (sudah diwakili sebagai susunan JavaScript):
[ [ 0, 0, 0], [-1, 1, 0], [ 0, 0, 0] ]
Dan (misalnya) kita melihat pada bagian dari gambar mana pixel terlihat seperti ini (setiap nomor mewakili kekuatan saluran warna merah; kita mengabaikan sisanya untuk kesederhanaan):
[ 00 ] [ 12 ] [ 43 ] [ 12 ] [ 56 ] [ 62 ] [ 63 ] [ 67 ] [ 92 ]
Dengan lilitan filter, kami adalah memodifikasi pixel di tengah (nilai saat ini: 56). Jadi kita mulai dengan mengalikan setiap nilai warna di sekitar pixel itu dengan yang pengganda dari lilitan array, dan kemudian kita menambahkan mereka bersama-sama. Kami mendapatkan persamaan berikut:
newPixelValue = (00 * 0) + (12 * 0) + (43 * 0) + (12 * -1) + (56 * 1) + (62 * 0) + (63 * 0) + (67 * 0) + (92 * 0) = (-12) + 56 = 44
Jadi sekarang kami menetapkan nilai kanal merah baru piksel ke 44 - tetapi dalam larik data baru, karena kita masih perlu mempertahankan nilai lama 56 untuk memodifikasi piksel lain dalam gambar. Ini berarti bahwa, ketika menerapkan filter, kita benar-benar membuat salinan gambar daripada memodifikasi ada satu di-tempat.
Dengan sebuah matriks yang lebih besar, Anda dapat melihat bagaimana rumus akan mendapatkan lebih kompleks, seperti setiap pixel tergantung pada data dari sekitarnya piksel lebih; untuk alasan ini, sebuah matriks yang lebih besar memerlukan waktu berjalan lebih lama. Ketika menjalankan filter lilitan, browser biasanya akan membeku untuk sementara.
Semua filter lanjutan yang akan kami buat akan bergantung pada filter ini, jadi akan lebih baik jika Anda memahami cara kerjanya. Sekali lagi, informasi lebih lanjut tersedia di sini.
Juga Anda harus ingat bahwa jumlah semua nilai di dalam matriks konvolusi harus sama dengan nol atau satu - jika tidak, Anda akan mendapatkan hasil yang aneh. Untuk membuat ini lebih sederhana kita dapat menggunakan faktor
dan variabel offset
. Setelah menghitung nilai piksel (dengan persamaan di atas), kita mengalikan seluruh nilai dengan faktor
dan menambahkan offset
. Ini menyederhanakan pembuatan, misalnya, filter blur (yang akan kita dapatkan dalam satu menit), di mana semua nilai matriks konvolusi adalah sama.
Sayangnya ada tidak ada implementasi ConvolutionFilter
di EaselJS, jadi kita harus menulis satu. Mengikuti contoh ColorFilter saya membuat kode ini:
(function (window) { var ConvolutionFilter = function (matrix, factor, offset) { this.initialize(matrix, factor, offset); } var p = ConvolutionFilter.prototype = new Filter(); p.matrix = null; p.factor = 0.0; p.offset = 0.0; p.initialize = function (matrix, factor, offset) { this.matrix = matrix; this.factor = factor; this.offset = offset; } p.applyFilter = function (ctx, x, y, width, height, targetCtx, targetX, targetY) { targetCtx = targetCtx || ctx; targetX = (targetX == null ? x: targetX); targetY = (targetY == null ? y: targetY); try { var imageData = ctx.getImageData(x, y, width, height); } catch (e) { return false; } var data = JSON.parse(JSON.stringify(imageData.data)); var matrixhalf = Math.floor(this.matrix.length / 2); var r = 0, g = 1, b = 2, a = 3; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var pixel = (y * width + x) * 4, sumr = 0, sumg = 0, sumb = 0; for (var matrixy in this.matrix) { for (var matrixx in this.matrix[matrixy]) { var convpixel = ((y + (matrixy - matrixhalf)) * width + (x + (matrixx - matrixhalf))) * 4; sumr += data[convpixel + r] * this.matrix[matrixy][matrixx]; sumg += data[convpixel + g] * this.matrix[matrixy][matrixx]; sumb += data[convpixel + b] * this.matrix[matrixy][matrixx]; } } imageData.data[pixel + r] = this.factor * sumr + this.offset; imageData.data[pixel + g] = this.factor * sumg + this.offset; imageData.data[pixel + b] = this.factor * sumb + this.offset; imageData.data[pixel + a] = data[pixel + a]; } } targetCtx.putImageData(imageData, targetX, targetY); return true; } p.toString = function() { return "[ConvolutionFilter]"; } p.clone = function() { return new ConvolutionFilter(this.matrix, this.factor, this.offset); } window.ConvolutionFilter = ConvolutionFilter; }(window));
Anda dapat melewati semua metode kecuali untuk applyFilter
, karena mereka digunakan oleh EaselJS untuk menginisialisasi filter.
applyFilter()
dipanggil ketika kami menerapkan filter ke gambar. Pertama kita harus mendapatkan data gambar dari kanvas, maka saya menggunakan trik dengan JSON.parse(JSON.stringify(imageData.data)) - karena kita ingin salinan data gambar dan imageData.data objek tidak memiliki metode clone() atau slice() untuk mencapai hal ini, jadi kami menggunakan ini sulit untuk benar-benar menyalin objek dan semua sifat-sifatnya.
Informasi warna disimpan dalam data ini seperti ini:
[ ... ][ red ][ green ][ blue ][ alpha ][ red ][ green ][ blue ][ alpha ][ ... ]
Jadi setiap piksel mengambil empat item array - satu untuk setiap saluran. Akhirnya, setelah iterasi melalui semua data pixel, kita memanggil putImageData() pada target untuk menyimpan hasilnya.
Langkah 22: Lilitan filter: kabur
Ini adalah efek konvolusi yang paling sederhana, jadi saya memutuskan untuk membiarkan pengguna mengatur radius filter (yang akan menghasilkan pengaturan ukuran array) untuk membuatnya lebih kompleks. Berikut adalah fungsi kita akan menggunakan:
filterBlur = function (radius) { var matrix = []; for (var y = 0; y < radius * 2; y++) { matrix[y] = []; for (var x = 0; x < radius * 2; x++) { matrix[y][x] = 1; } } applyFilter(new ConvolutionFilter(matrix, 1.0 / Math.pow(radius * 2, 2), 0.0)); hideDialog('#dialog-filterblur'); }
Ini menghasilkan matriks kabur, kemudian menerapkan filter.
Mengapa kita mengatur faktor untuk Math.pow (radius * 2, 2)? Karena seperti saya katakan sebelumnya: jumlah dari semua bidang array harus sama dengan nol atau satu; Jika kita membagi mereka semua dengan jumlah mereka kita akan selalu mendapatkan 1.
Di bawah ini adalah hasil dari filter ini:

Langkah 23: Gaussian Blur

Filter lilitan ini apa yang disebut karena menggunakan nilai-nilai dari standar distribusi Gaussian (digambarkan di atas) dimasukkan ke dalam matriks lilitan. Untuk menyederhanakan tugas kita membiarkan pengguna memilih hanya tiga nilai-nilai radius, karena menerapkan filter untuk radius lebih besar akan mengambil terlalu banyak waktu (3px radius sudah matriks 7 oleh 7).
Berikut adalah fungsi:
var gaussMatrix = [ [ [ 0.05472157, 0.11098164, 0.05472157 ], [ 0.11098164, 0.22508352, 0.11098164 ], [ 0.05472157, 0.11098164, 0.05472157 ] ], [ [ 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633 ], [ 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965 ], [ 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373 ], [ 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965 ], [ 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633 ] ], [ [ 0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067 ], [ 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292 ], [ 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117 ], [ 0.00038771, 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373, 0.00038771 ], [ 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117 ], [ 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292 ], [ 0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067 ] ] ]; filterGaussianBlur = function (radius) { applyFilter(new ConvolutionFilter(gaussMatrix[radius], 1.0, 0.0)); hideDialog('#dialog-filtergaussianblur'); }
Kita menetapkan matriks di luar fungsi sehingga tidak untuk membuang-buang waktu menetapkan setiap kali pengguna memilih itu dari menu. Dalam fungsi ini kami hanya memilih matriks tertentu dan menerapkan filter. Anda tentu saja dapat menambahkan lebih banyak nilai radius jika Anda mau.
Di bawah ini adalah hasil dari filter ini:

Langkah 24: Deteksi Tepi

Deteksi tepi adalah teknik yang sering digunakan dalam AI robot untuk membantu mereka bergerak, karena deteksi tepi hanya menyisakan tepi gambar. Hal ini juga efek yang benar-benar bagus untuk digunakan dalam seni.
Untuk mencapai ini kita menggunakan perkiraan nilai-nilai pertama dari distribusi Laplace (gambar di atas) dengan b = 1/4
. Semua fungsi dari titik ini hanya akan memiliki matriks yang berbeda:
filterEdgeDetection = function () { applyFilter(new ConvolutionFilter( [ [ 0, -1, 0 ], [ -1, 4, -1 ], [ 0, -1, 0 ] ], 1.0, 0.0 )); hideDialog('#dialog-filteredgedetection'); }
Di bawah ini adalah hasil dari filter ini:

Langkah 25: Edge meningkatkan
Filter ini memiliki efek yang sama dengan sebelumnya, tetapi meningkatkan tepi tanpa menghilangkan sisa gambar - membuatnya sempurna untuk penggunaan artistik. Ini adalah benar-benar matriks saya digunakan untuk menjelaskan kepada Anda bagaimana lilitan Filter bekerja:
filterEdgeEnhance = function () { applyFilter(new ConvolutionFilter( [ [ 0, 0, 0 ], [ -1, 1, 0 ], [ 0, 0, 0 ] ], 1.0, 0.0 )); hideDialog('#dialog-filteredgeenhance'); }
Di bawah ini adalah hasil dari filter ini:

Langkah 26: Emboss
Emboss filter menambah sedikit efek 3D gambar dengan menyoroti sudut kiri-bawah tepi (jadi itu juga penyaring deteksi tepi).
Fungsi:
filterEmboss = function () { applyFilter(new ConvolutionFilter( [ [ -1, -1, 0 ], [ -1, 1, 1 ], [ 0, 1, 1 ] ], 1.0, 0.0 )); hideDialog('#dialog-filteremboss'); }
Di bawah ini adalah hasil dari filter ini:

Langkah 27: Pertajam
Kita semua tahu apa mengasah. Ini dicapai dengan sedikit modifikasi deteksi tepi:
filterSharpen = function () { applyFilter(new ConvolutionFilter( [ [ 0, -1, 0 ], [ -1, 5, -1 ], [ 0, -1, 0 ] ], 1.0, 0.0 )); hideDialog('#dialog-filtersharpen'); }
Di bawah ini adalah hasil dari filter ini:

Anda lihat perbedaannya? Hal ini sangat kecil dalam lilitan matriks, tapi gambar dihasilkan lebih tajam.
Itu adalah filter terakhir, tetapi Anda dapat menambahkan lebih banyak jika Anda mau. Hanya mencari beberapa di Internet atau percobaan untuk membuat Anda sendiri yang unik.
Langkah 28: Filter Callback
Ini akan menjadi sekelompok panggilan jQuery berikutnya, tetapi sebelum itu kita memerlukan fungsi pembantu kedua kita di file ini:
filterSwitch = function (e, val, func) { switch (e.type) { case "click": func(val); break; case "keydown": if (e.keyCode == 13) func(val); break; } }
Dibutuhkan tiga parameter:
-
e
- objek acara, -
Val
- nilai untuk lulus ke fungsi, dan -
func
- fungsi untuk panggilan dengan nilai sebelumnya.
Ini menciptakan singkatan yang bisa kita gunakan dalam kode panggilan balik berikut; tempelkan saja di bawah helper:
app.callbacks.filterBrightness = function (e) { var val = $('#dialog-filterbrightness input').val() / 100; filterSwitch(e, val, filterBrightness); } app.callbacks.filterDesaturation = function () { filterDesaturation(); } app.callbacks.filterColorify = function (e) { var r = $('#dialog-filtercolorify input.r').val() * 1, g = $('#dialog-filtercolorify input.g').val() * 1, b = $('#dialog-filtercolorify input.b').val() * 1, a = $('#dialog-filtercolorify input.a').val() * 1; switch (e.type) { case "click": filterColorify(r, g, b, a); break; case "keydown": if (e.keyCode == 13) filterColorify(r, g, b, a); break; } } app.callbacks.filterBlur = function (e) { var val = $('#dialog-filterblur input').val() * 1; filterSwitch(e, val, filterBlur); } app.callbacks.filterGaussianBlur = function (e) { var val = ($('#dialog-filtergaussianblur input.3').attr('checked') ? 2: $('#dialog-filtergaussianblur input.2').attr('checked') ? 1: 0); filterSwitch(e, val, filterGaussianBlur); } app.callbacks.filterEdgeDetection = function (e) { filterEdgeDetection(); } app.callbacks.filterEdgeEnhance = function (e) { filterEdgeEnhance(); } app.callbacks.filterEmboss = function (e) { filterEmboss(); } app.callbacks.filterSharpen = function (e) { filterSharpen(); }
Langkah 29: Scripting: Pendahuluan
Mengizinkan pengguna untuk menggunakan beberapa bahasa scripting dalam aplikasi Anda adalah fitur yang sangat berguna. Ini memungkinkan pengguna untuk mengotomatisasi pekerjaannya, atau ketika dia mencapai beberapa efek yang bagus dia dapat membaginya dengan orang lain, dan orang ini juga akan mendapatkan efek yang sama. Dan karena kami menulis seluruh aplikasi dalam JavaScript - yang merupakan bahasa scripting itu sendiri - sangat mudah untuk membuat fitur semacam itu.
Menggunakan fungsi eval()
kita kita dapat menjalankan beberapa JavaScript dari string, dan string ini akan menjadi pengguna script.
Anda mungkin pernah membaca bahwa menggunakan fungsi eval()
adalah praktik yang sangat buruk. Tentu saja jika Anda harus menggunakannya di dalam kode Anda, maka kalimat ini benar, karena ini menonaktifkan semua cache atau kompilasi yang menggunakan mesin JavaScript modern untuk mempercepat kode. Ini juga menciptakan instance dari parser JavaScript, yang memboroskan memori sampai selesai dengan kode. Jadi hindari menggunakan eval()
seperti ini:
var myEvilCondition = "someObjectPropertyButIDontKnowWhichOne"; function evilEval () { return eval('myObject.' + myEvilCondition); }
Kode di atas sangat buruk. Anda tidak harus menggunakan eval() seperti ini. Contoh di atas dapat diperbaiki menggunakan tanda kurung siku:
var myNotEvilCondition = "someObjectPropertyButIDontKnowWhichOne"; function goodNoEval () { return myObject[myNotEvilCondition]; }
Dalam kasus kami semuanya baik-baik saja, karena menulis juru bahasa Anda sendiri akan sangat membuang waktu dan sumber daya (artinya lebih banyak atau lebih besar file untuk diunduh oleh pengguna).
Langkah 30: Skrip: aman Eval
Karena kami mengeksekusi skrip pengguna, kami harus memastikan bahwa dia tidak secara tidak sengaja merusak hasil yang dia kerjakan karena dia salah ketik nama fungsi atau nomor lapisan. Itulah mengapa ia harus memastikan bahwa pengguna tidak dapat mengakses setiap jendela atau app metode langsung.
Untuk alasan ini, kami membuat fungsi kami terlihat seperti ini:
scriptExecute = function (code) { hideDialog('#dialog-executescript'); if ((code.match(/eval\(/g) != null) && (!confirm('You used the eval function inside of your code. This may lead to unexpected effects, do you want to continue?'))) return; eval(code.replace(/(window\.|app\.)(.*?);/g, '')); }
(Meletakkan kode ini ke dalam scripts.js file.)
Kita menyembunyikan dialog pertama karena itu tidak hanya akan bertahan tidak sampai script selesai, dan mungkin pengguna ingin melihat ia script di tempat kerja. Kemudian kita sebut kode yang tersedia, tapi kami mengganti semua jendela dan app terkait panggilan, sehingga pengguna tidak dapat menghapus semua lapisan atau menutup jendela oleh kesalahan.
Sedikit peringatan di sini: pengguna masih bisa melakukan sesuatu dengan variabel ini jika ia menggunakan fungsi eval() dalam kode - tapi kemudian kita bertanya kepadanya Apakah dia benar-benar ingin melakukan ini.
Sekarang kita harus menambahkan fungsi callback kecil ini:
app.callbacks.scriptExecute = function (e) { scriptExecute($('#dialog-executescript textarea').val()); }
Itu adalah sistem script lengkap. Pergi ke depan dan check it out dengan melewati beberapa kode yang bagus untuk dialog 'Jalankan Script'. Coba gunakan eval()
di sana untuk melihat bahwa itu menanyakan Anda apakah Anda benar-benar ingin melakukan ini.
Kesimpulan
Seperti yang Anda lihat, HTML5 Canvas adalah hal yang sangat kuat. Tapi kita hanya menggores permukaan apa yang bisa dilakukan dengan itu. Kami telah membuat aplikasi yang sangat canggih - yang memungkinkan pengguna memuat foto mereka dan membuat beberapa modifikasi pada mereka, lalu menyimpan dan mencetak foto yang diedit - menggunakan JavaScript murni. Beberapa tahun yang lalu itu akan menjadi lelucon.
Juga Anda dipersilakan untuk memperluas aplikasi yang baru Anda buat! Tambahkan lebih banyak filter, ubah antarmuka, tambahkan fungsi yang lebih bermanfaat (misalnya Anda dapat menambahkan lebih banyak properti ke lapisan, mungkin daftar semua filter dengan kemungkinan menghapus dan mengeditnya, atau tombol untuk mengonversi buffer undo menjadi skrip siap untuk pengguna untuk berbagi). Jadilah kreatif dan mungkin Anda akan membuat beberapa hal yang sangat berguna!
Terima kasih untuk membaca tutorial ini, saya berharap saya benar-benar mengajarkan Anda sesuatu yang Anda akan menggunakan beberapa proyek yang besar. Jika Anda memerlukan bantuan dalam pembuatan sesuatu yang canggih dengan HTML5, jangan ragu untuk bertanya pada email kontak saya atau dengan menambahkan komentar ke tutorial ini. Saya akan menjawab Anda segera setelah saya mendapatkan pesan Anda.