├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── blank.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── V-logo.svg.png ├── demo │ ├── app.js │ ├── app.wasm │ ├── bkup │ │ ├── app.js │ │ └── app.wasm │ ├── index.html │ ├── iui_helper.js │ ├── titlebar.png │ └── vweb_example.v ├── index.html ├── previeww.png └── tweet.png ├── src ├── assets │ ├── checker2.png │ ├── hsv.png │ ├── icons8-eraser-tool-32.png │ ├── icons8-mouse-24.png │ ├── old │ │ ├── color-dropper.png │ │ ├── fill-can.png │ │ ├── icons8-drag-32.png │ │ ├── icons8-paint-sprayer-32.png │ │ ├── icons8-pencil-drawing-32.png │ │ ├── pencil-tip.png │ │ ├── resize.png │ │ └── select.png │ ├── rgb-picker.png │ ├── tools.png │ └── undo.png ├── blank.png ├── color_picker.v ├── image_resize.v ├── image_save.v ├── image_view.v ├── menubar.v ├── paint.v ├── resize_modal.v ├── ribbon.v ├── selection.v ├── settings.v ├── shapes.v ├── sidebar.v ├── statusbar.v ├── tools.v └── util.v ├── untitledv.png ├── v.mod └── v.png /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.v linguist-language=V text=auto eol=lf 2 | *.vv linguist-language=V text=auto eol=lf 3 | *.vsh linguist-language=V text=auto eol=lf 4 | **/v.mod linguist-language=V text=auto eol=lf 5 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: Build binary artifacts 2 | 3 | on: 4 | push: 5 | tags: 6 | - weekly.** 7 | - 0.** 8 | 9 | jobs: 10 | 11 | build-linux: 12 | runs-on: ubuntu-20.04 13 | env: 14 | CC: gcc 15 | ZIPNAME: vpaint_linux.zip 16 | steps: 17 | - name: Setup V 18 | uses: vlang/setup-v@v1.4 19 | with: 20 | # Default: ${{ github.token }} 21 | token: ${{ github.token }} 22 | version: 'weekly.2024.37' 23 | version-file: '' 24 | check-latest: true 25 | stable: false 26 | architecture: '' 27 | - uses: actions/checkout@v1 28 | - name: Compile 29 | run: | 30 | sudo apt-get -qq update 31 | sudo apt-get -qq install libgc-dev 32 | sudo apt install build-essential 33 | sudo apt-get --yes --force-yes install libxi-dev libxcursor-dev mesa-common-dev 34 | sudo apt-get --yes --force-yes install libgl1-mesa-glx 35 | v install https://github.com/pisaiah/ui 36 | git clone https://github.com/pisaiah/ui iui 37 | cd iui 38 | git clone https://github.com/pisaiah/vpaint --depth 1 39 | v -cc $CC -skip-unused -gc boehm vpaint 40 | - name: Remove excluded 41 | run: | 42 | rm -rf .git 43 | rm -rf docs 44 | cd iui 45 | cd vpaint 46 | rm -rf docs 47 | rm -rf *.v 48 | cd .. 49 | cd .. 50 | - name: Create ZIP archive 51 | run: | 52 | cd iui 53 | zip -r9 --symlinks $ZIPNAME vpaint/ 54 | mv $ZIPNAME ../ 55 | cd .. 56 | - name: Create artifact 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: linux 60 | path: vpaint_linux.zip 61 | 62 | build-macos: 63 | runs-on: macos-latest 64 | env: 65 | CC: clang 66 | ZIPNAME: vpaint_macos.zip 67 | steps: 68 | - name: Setup V 69 | uses: vlang/setup-v@v1.4 70 | with: 71 | # Default: ${{ github.token }} 72 | token: ${{ github.token }} 73 | version: 'weekly.2024.37' 74 | version-file: '' 75 | check-latest: true 76 | stable: false 77 | architecture: '' 78 | - uses: actions/checkout@v1 79 | - name: Compile 80 | run: | 81 | v install https://github.com/pisaiah/ui 82 | git clone https://github.com/pisaiah/ui iui 83 | cd iui 84 | git clone https://github.com/pisaiah/vpaint --depth 1 85 | v -cc $CC -skip-unused -gc boehm vpaint 86 | - name: Remove excluded 87 | run: | 88 | rm -rf .git 89 | rm -rf docs 90 | cd iui 91 | cd vpaint 92 | rm -rf docs 93 | rm -rf *.v 94 | cd .. 95 | cd .. 96 | - name: Create ZIP archive 97 | run: | 98 | cd iui 99 | zip -r9 --symlinks $ZIPNAME vpaint/ 100 | mv $ZIPNAME ../ 101 | cd .. 102 | - name: Create artifact 103 | uses: actions/upload-artifact@v4 104 | with: 105 | name: macos 106 | path: vpaint_macos.zip 107 | 108 | build-windows: 109 | runs-on: windows-latest 110 | env: 111 | CC: msvc 112 | ZIPNAME: vpaint_windows.zip 113 | steps: 114 | - name: Setup V 115 | uses: vlang/setup-v@v1.4 116 | with: 117 | # Default: ${{ github.token }} 118 | token: ${{ github.token }} 119 | version: 'weekly.2024.37' 120 | version-file: '' 121 | check-latest: true 122 | stable: false 123 | architecture: '' 124 | - uses: actions/checkout@v1 125 | - uses: msys2/setup-msys2@v2 126 | - name: Compile 127 | run: | 128 | where v 129 | v install https://github.com/pisaiah/ui 130 | git clone https://github.com/pisaiah/vpaint --depth 1 131 | v -cc gcc -skip-unused -gc boehm -cflags -static -cflags -mwindows vpaint 132 | - name: Remove excluded 133 | shell: msys2 {0} 134 | run: | 135 | rm -rf .git 136 | rm -rf docs 137 | cd vpaint 138 | rm -rf docs 139 | rm -rf *.v 140 | cd .. 141 | - name: Create archive 142 | shell: msys2 {0} 143 | run: | 144 | cd vpaint 145 | cd .. 146 | powershell Compress-Archive vpaint $ZIPNAME 147 | # NB: the powershell Compress-Archive line is from: 148 | # https://superuser.com/a/1336434/194881 149 | # It is needed, because `zip` is not installed by default :-| 150 | - name: Create artifact 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: windows 154 | path: vpaint_windows.zip 155 | build-emscripten: 156 | env: 157 | EM_VERSION: 1.39.18 158 | EM_CACHE_FOLDER: 'emsdk-cache' 159 | CC: gcc 160 | ZIPNAME: vpaint_emscripten.zip 161 | runs-on: ubuntu-latest 162 | steps: 163 | - uses: mymindstorm/setup-emsdk@v14 164 | - name: Setup emsdk 165 | uses: mymindstorm/setup-emsdk@v14 166 | with: 167 | # Make sure to set a version number! 168 | version: 3.1.67 169 | # This is the name of the cache folder. 170 | # The cache folder will be placed in the build directory, 171 | # so make sure it doesn't conflict with anything! 172 | actions-cache-folder: 'emsdk-cache' 173 | - name: Verify 174 | run: emcc -v 175 | - name: Setup V 176 | uses: vlang/setup-v@v1.4 177 | with: 178 | # Default: ${{ github.token }} 179 | token: ${{ github.token }} 180 | version: 'weekly.2024.37' 181 | version-file: '' 182 | check-latest: true 183 | stable: false 184 | architecture: '' 185 | - uses: actions/checkout@v1 186 | - name: Compile 187 | run: | 188 | sudo apt-get -qq update 189 | sudo apt-get -qq install libgc-dev 190 | sudo apt install build-essential 191 | sudo apt-get --yes --force-yes install libxi-dev libxcursor-dev mesa-common-dev 192 | sudo apt-get --yes --force-yes install libgl1-mesa-glx 193 | v install https://github.com/pisaiah/ui 194 | git clone https://github.com/pisaiah/ui iui 195 | cd iui 196 | git clone https://github.com/vlang/v --depth 1 197 | cd v 198 | make 199 | mv v v.exe 200 | git clone https://github.com/pisaiah/v-emscripten-script 201 | cd v-emscripten-script 202 | ../v.exe . -o v2w.exe 203 | git clone https://github.com/pisaiah/vpaint --depth 1 204 | dir 205 | ./v2w.exe ../ vpaint -Os 206 | - name: Remove excluded 207 | run: | 208 | rm -rf .git 209 | - name: Create ZIP archive 210 | run: | 211 | cd iui 212 | cd v 213 | zip -r9 --symlinks $ZIPNAME v-emscripten-script/output/ 214 | mv $ZIPNAME ../../ 215 | cd .. 216 | cd .. 217 | - name: Create artifact 218 | uses: actions/upload-artifact@v4 219 | with: 220 | name: emscripten 221 | path: vpaint_emscripten.zip 222 | 223 | release: 224 | name: Create Github Release 225 | needs: [build-linux, build-windows, build-macos, build-emscripten] 226 | runs-on: ubuntu-20.04 227 | steps: 228 | - name: Get short tag name 229 | uses: winterjung/split@v2 230 | id: split 231 | with: 232 | msg: ${{ github.ref }} 233 | separator: / 234 | - name: Create Release 235 | id: create_release 236 | uses: ncipollo/release-action@v1 237 | with: 238 | token: ${{ secrets.GITHUB_TOKEN }} 239 | tag: ${{ steps.split.outputs._2 }} 240 | name: ${{ steps.split.outputs._2 }} 241 | commit: ${{ github.sha }} 242 | draft: false 243 | prerelease: false 244 | 245 | publish: 246 | needs: [release] 247 | runs-on: ubuntu-20.04 248 | strategy: 249 | matrix: 250 | version: [linux, macos, windows, emscripten] 251 | steps: 252 | - uses: actions/checkout@v1 253 | - name: Fetch artifacts 254 | uses: actions/download-artifact@v4 255 | with: 256 | name: ${{ matrix.version }} 257 | path: ./${{ matrix.version }} 258 | - name: Get short tag name 259 | uses: winterjung/split@v2 260 | id: split 261 | with: 262 | msg: ${{ github.ref }} 263 | separator: / 264 | - name: Get release 265 | id: get_release_info 266 | uses: leahlundqvist/get-release@v1.3.1 267 | env: 268 | GITHUB_TOKEN: ${{ github.token }} 269 | with: 270 | tag_name: ${{ steps.split.outputs._2 }} 271 | - name: Upload Release Asset 272 | id: upload-release-asset 273 | uses: actions/upload-release-asset@v1.0.1 274 | env: 275 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 276 | with: 277 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 278 | asset_path: ${{ matrix.version }}/vpaint_${{ matrix.version }}.zip 279 | asset_name: vpaint_${{ matrix.version }}.zip 280 | asset_content_type: application/zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | paint 4 | vpaint 5 | *.exe 6 | *.exe~ 7 | *.so 8 | *.dylib 9 | *.dll 10 | vuntitled.png 11 | untitledv.png 12 | untitledv*.png 13 | vls.log 14 | desktop.ini 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Isaiah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vPaint ![GitHub](https://img.shields.io/badge/license-MIT-blue?style=flat) ![vlang](http://img.shields.io/badge/V-0.4.8-%236d8fc5?style=flat) 2 | ## About 3 | Image Viewer & Editor written in the [V](https://vlang.io) Programming Language. 4 | 5 | ![image](https://github.com/user-attachments/assets/86ba1ce5-1ac4-46ca-98e8-d447cf46b28e) 6 | 7 | 8 | Online demo: [https://vpaint.app/demo](https://pisaiah.com/vpaint/demo/) 9 | 10 | ## Screenshots 11 | 12 |
Color Picker (Compact)
13 |
Color Picker (Wide)
14 |
Selection Move
15 |
Settings Page
16 |
Menus
17 | 18 | ## Compile dependencies 19 | - [iUI](https://github.com/pisaiah/ui) 20 | - V 0.4.8 or higher 21 | 22 | ## Credits 23 | - V - [https://vlang.io](https://vlang.io) 24 | - Icons by [Icons8.com](https://icons8.com/) 25 | 26 | > Copyright (c) 2022-2024 Isaiah. 27 | [![GitHub](https://img.shields.io/badge/license-MIT-blue?style=flat)](https://opensource.org/license/mit/) 28 | -------------------------------------------------------------------------------- /docs/V-logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/V-logo.svg.png -------------------------------------------------------------------------------- /docs/demo/app.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/demo/app.wasm -------------------------------------------------------------------------------- /docs/demo/bkup/app.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/demo/bkup/app.wasm -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | vpaint Demo 23 | 24 |
25 |
26 |

27 |
28 | 29 |
30 | 31 | 32 | 66 | 67 | -------------------------------------------------------------------------------- /docs/demo/iui_helper.js: -------------------------------------------------------------------------------- 1 | // (c) 2024 Isaiah. 2 | import loadWASM from "./app.js"; 3 | 4 | function doo_load_files() { 5 | setTimeout(function() { 6 | for (var i = 0; i < localStorage.length; i++) { var key = localStorage.key(i); 7 | if (key.endsWith(".ttf")) { continue; } 8 | write_file(key.replace("//", "/"), localStorage.getItem(key)); 9 | } 10 | }, 1); 11 | } 12 | 13 | function write_file(a, b) { 14 | setTimeout(function() { 15 | var dirr = a.substring(0, a.lastIndexOf("/")); var dd = ""; 16 | var spl = dirr.split("/"); 17 | for (var i = 0; i < spl.length; i++) { var da = spl[i]; dd += da + "/"; try { iui.module.FS.mkdir(dd) } catch (exx) {} } 18 | try { iui.module.FS.mkdir(dirr) } catch (exx) {} 19 | iui.module.FS.writeFile(a, b); 20 | }, 1); 21 | } 22 | 23 | function save_folder(pa) { 24 | setTimeout(function() { save_folder_2(pa); }, 1); // wasm crashes if we load FS too early 25 | } 26 | 27 | function save_folder_2(pa) { 28 | var is_file = iui.module.FS.isFile(iui.module.FS.stat(pa).mode) 29 | if (!is_file) { 30 | var last = pa.substring(pa.lastIndexOf("/") + 1, pa.length); 31 | if (last.length <= 2 && last.includes(".")) { return; } 32 | var lss = iui.module.FS.readdir(pa); for (var i = 0; i < lss.length; i++) { save_folder_2(pa + "/" + lss[i]); } 33 | } else { var con = iui.module.FS.readFile(pa, { encoding: "utf8" }); localStorage.setItem(pa, con); } 34 | } 35 | 36 | window.iui = { 37 | module: null, 38 | latest_file: null, 39 | task_result: "0", 40 | open_file_dialog: async () => { 41 | let input = document.createElement("input"); 42 | input.type = "file"; 43 | await new Promise((promise_resolve, promise_reject) => { 44 | input.addEventListener("change", async e => { 45 | iui.latest_file = e.target.files[0]; 46 | let arr_buf = await iui.latest_file.arrayBuffer(); 47 | iui.module.FS.writeFile(iui.latest_file.name, new Uint8Array(arr_buf)); 48 | promise_resolve(); 49 | }); 50 | input.click(); 51 | }); 52 | iui.task_result = "1"; 53 | return iui.latest_file.name; 54 | }, 55 | save_file_dialog: async () => { 56 | iui.latest_file = {name: prompt("File Name to save to")}; 57 | try { iui.module.FS.unlink(iui.latest_file.name); } catch (error) {} 58 | iui.task_result = "1"; iui.watch_file_until_action(); 59 | }, 60 | download_file: (filename, uia) => { 61 | let blob = new Blob([uia], { type: "application/octet-stream" }); 62 | let url = window.URL.createObjectURL(blob); 63 | let downloader = document.createElement("a"); 64 | downloader.href = url; downloader.download = filename; downloader.click(); downloader.remove(); 65 | setTimeout(() => { window.URL.revokeObjectURL(url); }, 1000); 66 | }, 67 | watch_file_until_action: async () => { 68 | let fi_nam = iui.latest_file.name; 69 | let watcher = setInterval(() => { 70 | if(iui.module.FS.analyzePath(fi_nam).exists){ clearInterval(watcher); iui.download_file(fi_nam, iui.module.FS.readFile(fi_nam)); iui.module.FS.unlink(fi_nam); } 71 | }, 500); 72 | }, 73 | set trigger(val){ 74 | if (val == "openfiledialog"){ return iui.open_file_dialog(); } else if (val == "savefiledialog"){ iui.save_file_dialog(); } 75 | else if (val == "keyboard-hide"){ document.getElementById("canvas").focus(); navigator.virtualKeyboard.hide(); } 76 | else if (val == "keyboard-show"){ document.getElementById("canvas").focus(); navigator.virtualKeyboard.show(); } 77 | else if (val == "lloadfiles") { doo_load_files(); } else if (val == "savefiles") { save_folder("/home") } 78 | else if (val.indexOf("savefile=") != -1) { var fi_nam = val.split("savefile=")[1]; iui.download_file(fi_nam, iui.module.FS.readFile(fi_nam)); } 79 | } 80 | }; 81 | 82 | (async () => { 83 | iui.module = await loadWASM(); 84 | })(); 85 | -------------------------------------------------------------------------------- /docs/demo/titlebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/demo/titlebar.png -------------------------------------------------------------------------------- /docs/demo/vweb_example.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import rand 5 | import os 6 | 7 | const port = 8080 8 | 9 | struct App { 10 | vweb.Context 11 | mut: 12 | state shared State 13 | } 14 | 15 | struct State { 16 | mut: 17 | cnt int 18 | } 19 | 20 | fn main() { 21 | mut app := &App{} 22 | app.mount_static_folder_at(os.resource_abs_path('.'), '/') 23 | vweb.run_at(app, vweb.RunParams{ host: '192.168.2.23', port: 8080, family: .ip }) or { 24 | panic(err) 25 | } 26 | } 27 | 28 | pub fn (mut app App) index() vweb.Result { 29 | lock app.state { 30 | app.state.cnt++ 31 | } 32 | 33 | app.handle_static('assets', true) 34 | 35 | return app.file(os.resource_abs_path('index.html')) 36 | } 37 | 38 | @['/app.wasm'] 39 | pub fn (mut app App) was() vweb.Result { 40 | return app.file(os.resource_abs_path('app.wasm')) 41 | } 42 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 39 | vpaint 40 | 44 |
45 |
46 |
47 |

vPaint

48 |

Simple Image Viewer & Editor written in the V Programming Language.

49 |

 

50 |
51 | 52 | 53 |
54 |

Downloads:

55 | 60 | 61 |
62 |
63 |
64 | 65 |
66 |
Hover to load demo
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | © 2022-24 Isaiah. 79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |

Simple Image Viewer & Editor written in the V Programming Language.
90 |

91 |
92 |
93 |
94 | Links: 95 | 100 |
101 |
102 | Other: 103 | 107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |

Copyright © 2022-2024 Isaiah.

115 |
116 | 135 | 152 | 153 | -------------------------------------------------------------------------------- /docs/previeww.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/previeww.png -------------------------------------------------------------------------------- /docs/tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/docs/tweet.png -------------------------------------------------------------------------------- /src/assets/checker2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/checker2.png -------------------------------------------------------------------------------- /src/assets/hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/hsv.png -------------------------------------------------------------------------------- /src/assets/icons8-eraser-tool-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/icons8-eraser-tool-32.png -------------------------------------------------------------------------------- /src/assets/icons8-mouse-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/icons8-mouse-24.png -------------------------------------------------------------------------------- /src/assets/old/color-dropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/color-dropper.png -------------------------------------------------------------------------------- /src/assets/old/fill-can.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/fill-can.png -------------------------------------------------------------------------------- /src/assets/old/icons8-drag-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/icons8-drag-32.png -------------------------------------------------------------------------------- /src/assets/old/icons8-paint-sprayer-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/icons8-paint-sprayer-32.png -------------------------------------------------------------------------------- /src/assets/old/icons8-pencil-drawing-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/icons8-pencil-drawing-32.png -------------------------------------------------------------------------------- /src/assets/old/pencil-tip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/pencil-tip.png -------------------------------------------------------------------------------- /src/assets/old/resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/resize.png -------------------------------------------------------------------------------- /src/assets/old/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/old/select.png -------------------------------------------------------------------------------- /src/assets/rgb-picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/rgb-picker.png -------------------------------------------------------------------------------- /src/assets/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/tools.png -------------------------------------------------------------------------------- /src/assets/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/assets/undo.png -------------------------------------------------------------------------------- /src/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/src/blank.png -------------------------------------------------------------------------------- /src/color_picker.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gx 5 | import math 6 | 7 | const hue_deg = 359 8 | const modal_width = 445 9 | const compact_width = 360 10 | 11 | @[heap] 12 | struct ColorPicker { 13 | mut: 14 | bw int = 256 15 | btn &ui.Button 16 | p &ui.Panel 17 | slid &ui.Slider 18 | aslid &ui.Slider 19 | fields []&ui.TextField 20 | h f64 21 | s int 22 | v f64 23 | mx int 24 | my int 25 | color gx.Color 26 | events map[string][]fn (voidptr) 27 | } 28 | 29 | pub fn (mut cp ColorPicker) call_event() { 30 | for f in cp.events['color_picked'] { 31 | f(cp) 32 | } 33 | } 34 | 35 | pub fn (mut cp ColorPicker) default_modal_close_fn(mut e ui.MouseEvent) { 36 | mut win := e.ctx.win 37 | cp.call_event() 38 | win.components = win.components.filter(mut it !is ui.Modal) 39 | } 40 | 41 | fn modal_draw(mut e ui.DrawEvent) { 42 | h := e.target.height 43 | 44 | if h > 1 && h < 700 { 45 | mut tar := e.target 46 | if mut tar is ui.Modal { 47 | // tar.top_off = top_off 48 | } 49 | } 50 | 51 | if h < 0 { 52 | return 53 | } 54 | 55 | // Responsive Size 56 | mut tar := e.target 57 | if mut tar is ui.Modal { 58 | wss := e.ctx.gg.window_size() 59 | ws := wss.width 60 | 61 | if ws < modal_width { 62 | tar.in_width = compact_width 63 | tar.in_height = 395 64 | } else { 65 | tar.in_width = modal_width 66 | tar.in_height = 330 67 | } 68 | 69 | top_off := if h < tar.in_height + 120 { 2 } else { 50 } 70 | tar.top_off = top_off 71 | 72 | tar.children[0].width = tar.in_width 73 | tar.children[1].y = tar.in_height - 50 74 | tar.children[2].y = tar.in_height - 50 75 | 76 | bw := (tar.in_width - 20) / 2 77 | tar.children[1].width = bw - 20 78 | tar.children[2].x = bw + 5 // 20 79 | tar.children[2].width = bw - 5 80 | } 81 | } 82 | 83 | pub fn (mut cp ColorPicker) subscribe_event(val string, f fn (voidptr)) { 84 | cp.events[val] << f 85 | } 86 | 87 | fn ColorPicker.new() &ColorPicker { 88 | return &ColorPicker{ 89 | p: unsafe { nil } 90 | btn: ui.Button.new() 91 | slid: ui.Slider.new(min: 0, max: 100, dir: .vert) 92 | aslid: ui.Slider.new(min: 0, max: 255, dir: .vert) 93 | } 94 | } 95 | 96 | fn (mut cp ColorPicker) open_color_picker(c ?gx.Color) &ui.Modal { 97 | mut m := ui.Modal.new(title: '') 98 | 99 | m.subscribe_event('draw', modal_draw) 100 | m.needs_init = false 101 | m.in_width = modal_width 102 | m.in_height = 335 103 | m.top_off = 20 104 | 105 | mut p := cp.make_picker_panel(m.in_width, m.in_height) 106 | m.add_child(p) 107 | 108 | // Load Given gx.Color 109 | if c != none { 110 | cp.load_rgb(c) 111 | cp.update_text_fields() 112 | } 113 | 114 | // close btn 115 | mut close := m.make_close_btn(false) 116 | y := 292 117 | 118 | close.subscribe_event('mouse_up', cp.default_modal_close_fn) 119 | close.set_bounds(20, y, 200, 30) 120 | close.set_accent_filled(true) 121 | 122 | mut can := m.make_close_btn(true) 123 | can.text = 'Cancel' 124 | can.set_bounds(227, y, 200, 30) 125 | 126 | return m 127 | } 128 | 129 | fn (mut cp ColorPicker) make_picker_panel(w int, h int) &ui.Panel { 130 | if !isnil(cp.p) { 131 | return cp.p 132 | } 133 | 134 | // Create panel 135 | mut p := ui.Panel.new() 136 | p.set_bounds(5, 0, w - 10, h) 137 | cp.p = p 138 | 139 | mut btn := cp.btn 140 | mut slid := cp.slid 141 | mut aslid := cp.aslid 142 | 143 | // set bounds 144 | btn.set_bounds(0, 0, cp.bw, cp.bw) 145 | slid.set_bounds(0, 0, 38, 256) 146 | aslid.set_bounds(0, 0, 30, 256) // old h: 192 147 | 148 | // Add to panel 149 | p.add_child(btn) 150 | p.add_child(slid) 151 | p.add_child(aslid) 152 | 153 | // Add events 154 | btn.subscribe_event('after_draw', cp.hsl_btn_draw_evnt) 155 | slid.subscribe_event('after_draw', cp.slid_draw_evnt) 156 | aslid.subscribe_event('after_draw', cp.aslid_draw_evnt) 157 | slid.subscribe_event('value_change', cp.slid_value_change) 158 | aslid.subscribe_event('value_change', cp.aslid_value_change) 159 | 160 | mut fields_panel := cp.make_fields() 161 | p.add_child(fields_panel) 162 | 163 | return p 164 | } 165 | 166 | fn roun(a f64, place int) string { 167 | return '${a}'.substr_ni(0, place) 168 | } 169 | 170 | fn (mut cp ColorPicker) slid_draw_evnt(mut e ui.DrawEvent) { 171 | mut com := e.target // todo 172 | 173 | for i in 0 .. 51 { 174 | v := 100 - (i * 2) 175 | vp := f32(v) / 100 176 | color := hsv_to_rgb(cp.h, f32(cp.s) / 100, vp) 177 | y := com.ry + (5 * i) 178 | e.ctx.gg.draw_rect_filled(com.rx, y, com.width - 1, 6, color) 179 | } 180 | 181 | if mut com is ui.Slider { 182 | per := com.cur / com.max 183 | ts := 12 184 | wid := (com.height * per) - per * ts 185 | e.ctx.gg.draw_rounded_rect_filled(com.rx, com.ry + wid, com.width, ts, 32, e.ctx.theme.scroll_bar_color) 186 | e.ctx.gg.draw_rounded_rect_empty(com.rx, com.ry + wid, com.width, ts, 32, gx.black) 187 | } 188 | } 189 | 190 | fn (mut cp ColorPicker) aslid_draw_evnt(mut e ui.DrawEvent) { 191 | mut win := e.ctx.win 192 | mut com := e.target 193 | 194 | cpc := cp.color 195 | lenth := 16 196 | space := 16 197 | 198 | aa := gx.rgb(150, 150, 150) 199 | bb := gx.rgb(255, 255, 255) 200 | 201 | mut cc := false 202 | for i in 0 .. lenth { 203 | val := 255 - (i * space) 204 | color := gx.rgba(cpc.r, cpc.g, cpc.b, u8(val)) 205 | y := com.ry + space * i 206 | 207 | ca := if cc { aa } else { bb } 208 | cb := if cc { bb } else { aa } 209 | fw := com.width / 2 210 | 211 | win.gg.draw_rect_filled(com.rx, y, fw, space, ca) 212 | win.gg.draw_rect_filled(com.rx + fw, y, fw - 1, space, cb) 213 | win.gg.draw_rect_filled(com.rx, y, com.width - 1, space, color) 214 | cc = !cc 215 | } 216 | 217 | if mut com is ui.Slider { 218 | com.thumb_wid = 1 219 | mut per := com.cur / com.max 220 | ts := 12 221 | wid := (com.height * per) - per * ts 222 | win.gg.draw_rounded_rect_filled(com.rx, com.ry + wid, com.width, ts, 8, win.theme.scroll_bar_color) 223 | win.gg.draw_rounded_rect_empty(com.rx, com.ry + wid, com.width - 1, ts, 8, gx.black) 224 | } 225 | } 226 | 227 | fn (mut cp ColorPicker) aslid_value_change(mut e ui.FloatValueChangeEvent) { 228 | cur := 255 - u8(e.target.cur) 229 | cp.fields[3].text = '${cur}' 230 | cp.update_text_fields_if_need() 231 | cp.update_color() 232 | } 233 | 234 | fn (mut cp ColorPicker) slid_value_change(mut e ui.FloatValueChangeEvent) { 235 | cp.v = 100 - e.target.cur 236 | cp.update_text_fields_if_need() 237 | cp.update_color() 238 | } 239 | 240 | fn p1_draw_responsive(mut e ui.DrawEvent) { 241 | mut p1 := e.get_target[ui.Panel]() 242 | wss := e.ctx.gg.window_size() 243 | ws := wss.width 244 | 245 | if ws < modal_width { 246 | if mut p1.layout is ui.BoxLayout { 247 | p1.layout = ui.GridLayout.new(cols: 4) 248 | } 249 | p1.width = compact_width - 20 250 | p1.height = 32 * 2 251 | } else { 252 | if mut p1.layout is ui.GridLayout { 253 | p1.layout = ui.BoxLayout.new(ori: 1, hgap: 4, vgap: 8) 254 | p1.width = 0 255 | p1.height = 0 256 | } 257 | } 258 | } 259 | 260 | fn (mut cp ColorPicker) make_fields() &ui.Panel { 261 | mut p1 := ui.Panel.new(layout: ui.BoxLayout.new(ori: 1, hgap: 4, vgap: 8)) 262 | mut l1 := ui.Label.new(text: 'HSV', pack: true, vertical_align: .middle) 263 | 264 | p1.subscribe_event('draw', p1_draw_responsive) 265 | p1.add_child(l1) 266 | 267 | for val in ['H', 'S', 'V', 'A'] { 268 | if val == 'A' { 269 | mut lbl := ui.Label.new(text: 'Alpha', pack: true, vertical_align: .middle) 270 | p1.add_child(lbl) 271 | } 272 | 273 | mut f := ui.numeric_field(255) 274 | f.subscribe_event('draw', numfield_draw_evnt) 275 | f.subscribe_event('text_change', cp.hsv_num_box_change_evnt) 276 | p1.add_child(f) 277 | cp.fields << f 278 | } 279 | 280 | for val in ['RGB'] { 281 | mut f := ui.TextField.new(text: '255, 255, 255,') 282 | f.subscribe_event('draw', numfield_draw_evnt) 283 | f.numeric = true 284 | 285 | mut lbl := ui.Label.new(text: val, pack: true, vertical_align: .middle) 286 | f.subscribe_event('text_change', cp.rgb_num_box_change_evnt) 287 | 288 | p1.add_child(lbl) 289 | p1.add_child(f) 290 | cp.fields << f 291 | } 292 | 293 | return p1 294 | } 295 | 296 | fn numfield_draw_evnt(mut e ui.DrawEvent) { 297 | if e.target.text.contains(',') { 298 | e.target.width = e.ctx.text_width('255, 255, 255') + 10 299 | return 300 | } 301 | 302 | if e.target.parent.width > 150 { 303 | // Let GridLayout do size 304 | return 305 | } 306 | 307 | e.target.width = e.target.parent.width - 5 // e.ctx.text_width('255,255') 308 | } 309 | 310 | fn (mut cp ColorPicker) rgb_num_box_change_evnt(mut e ui.TextChangeEvent) { 311 | colors := cp.fields[4].text.replace(' ', '').split(',') 312 | 313 | if colors.len < 3 { 314 | mut nt := []string{} 315 | for col in colors { 316 | nt << col 317 | } 318 | for nt.len < 3 { 319 | nt << '0' 320 | } 321 | cp.fields[4].text = nt.join(', ') 322 | 323 | return 324 | } 325 | 326 | r := colors[0].u8() 327 | g := colors[1].u8() 328 | b := colors[2].u8() 329 | 330 | if colors.len > 3 { 331 | cp.fields[4].text = '0, 0, 0' 332 | } 333 | 334 | a := cp.fields[3].text.u8() 335 | cp.load_rgb(gx.rgba(r, g, b, a)) 336 | } 337 | 338 | // Turn mouse down (mx, my) into HSV 339 | fn (mut cp ColorPicker) do_hsv_mouse_down(wmx int, wmy int) { 340 | cp.mx = math.min(wmx, cp.btn.rx + cp.btn.width) 341 | cp.my = math.min(wmy, cp.btn.ry + cp.btn.height) 342 | cp.mx = math.max(cp.btn.rx, cp.mx) - cp.btn.rx 343 | cp.my = math.max(cp.btn.ry, cp.my) - cp.btn.ry 344 | 345 | w := cp.bw 346 | cp.h = (f32(cp.mx) / w) 347 | cp.s = int((f32(w - (cp.my)) / w) * 100) 348 | cp.v = 100 - cp.slid.cur 349 | 350 | cp.update_color() 351 | cp.update_text_fields_if_need() 352 | } 353 | 354 | fn (mut cp ColorPicker) update_color() { 355 | color := hsv_to_rgb(cp.h, f32(cp.s) / 100, f32(cp.v) / 100) 356 | alpha := cp.fields[3].text.u8() 357 | cp.color = gx.rgba(color.r, color.g, color.b, alpha) 358 | } 359 | 360 | fn (mut cp ColorPicker) update_text_fields_if_need() { 361 | cp.update_text_fields() 362 | } 363 | 364 | fn (mut cp ColorPicker) update_text_fields() { 365 | cp.fields[0].text = '${int(cp.h * hue_deg)}' 366 | cp.fields[1].text = '${cp.s}' 367 | cp.fields[2].text = '${int(cp.v)}' 368 | cp.fields[4].text = '${cp.color.r}, ${cp.color.g}, ${cp.color.b}' 369 | cp.update_fields_pos() 370 | } 371 | 372 | fn (mut cp ColorPicker) hsl_btn_draw_evnt(mut e ui.DrawEvent) { 373 | mut win := e.ctx.win 374 | 375 | if e.target.is_mouse_down { 376 | cp.do_hsv_mouse_down(win.mouse_x, win.mouse_y) 377 | } 378 | 379 | if cp.btn.icon == -1 { 380 | mut cim := 0 381 | if 'HSL' in win.id_map { 382 | hsl := &int(unsafe { win.id_map['HSL'] }) 383 | cim = *hsl 384 | } 385 | cp.btn.icon = cim 386 | } 387 | 388 | x := cp.mx - 7 + e.target.rx 389 | e.ctx.gg.draw_rounded_rect_empty(x, cp.my - 7 + e.target.ry, 16, 16, 32, gx.white) 390 | e.ctx.gg.draw_rounded_rect_empty(x - 1, cp.my - 8 + e.target.ry, 16, 16, 32, gx.black) 391 | 392 | ty := cp.btn.ry - 24 - 8 // cp.btn.ry + cp.btn.height + 4 393 | 394 | e.ctx.gg.draw_rect_filled(cp.btn.rx, ty, cp.bw, 24, cp.color) 395 | 396 | br := f32(cp.color.r) * 299 397 | bg := f32(cp.color.g) * 587 398 | bb := f32(cp.color.b) * 114 399 | o := (br + bg + bb) / 1000 400 | tco := if o > 125 { gx.black } else { gx.white } 401 | 402 | e.ctx.gg.draw_text(cp.btn.rx + 5, ty + 4, '${cp.color.to_css_string()}', gx.TextCfg{ 403 | size: e.ctx.font_size 404 | color: tco 405 | }) 406 | 407 | // Draw color 408 | e.ctx.gg.draw_rect_empty(e.target.rx, e.target.ry, e.target.width, e.target.height, 409 | gx.black) 410 | e.ctx.gg.draw_rect_empty(e.target.rx - 1, e.target.ry - 1, e.target.width + 2, 411 | e.target.height + 2, cp.color) 412 | } 413 | 414 | const field_max = [359, 100, 100] 415 | 416 | fn (mut cp ColorPicker) hsv_num_box_change_evnt(mut e ui.TextChangeEvent) { 417 | for i, max in field_max { 418 | if cp.fields[i].text.int() > max { 419 | cp.fields[i].text = '${max}' 420 | } 421 | } 422 | 423 | h := cp.fields[0].text.int() 424 | s := cp.fields[1].text.int() 425 | v := cp.fields[2].text.int() 426 | 427 | cp.load_hsv_int(h, s, v) 428 | cp.update_hsv_m() 429 | } 430 | 431 | fn (mut cp ColorPicker) load_hsv_int(h int, s int, v int) { 432 | cp.h = f32(h) / hue_deg 433 | cp.s = s 434 | cp.v = v 435 | 436 | cp.slid.cur = f32(100 - cp.v) 437 | cp.aslid.cur = 255 - cp.fields[3].text.u8() 438 | 439 | cp.update_color() 440 | cp.update_hsv_m() 441 | } 442 | 443 | fn (mut cp ColorPicker) load_rgb(color gx.Color) { 444 | mut h, s, v := rgb_to_hsv(color) 445 | 446 | cp.h = h 447 | cp.s = int(f32(s) * 100) 448 | cp.v = 100 * v 449 | alpha := color.a 450 | 451 | cp.fields[3].text = '${alpha}' 452 | cp.slid.cur = f32(100 - cp.v) 453 | cp.aslid.cur = 255 - alpha 454 | cp.color = gx.rgba(color.r, color.g, color.b, alpha) 455 | 456 | cp.update_hsv_m() 457 | cp.update_text_fields() 458 | } 459 | 460 | fn (mut cp ColorPicker) update_fields_pos() { 461 | for mut f in cp.fields { 462 | if f.is_selected { 463 | continue 464 | } 465 | 466 | f.carrot_left = f.text.len 467 | } 468 | } 469 | 470 | fn (mut cp ColorPicker) update_hsv_m() { 471 | w := cp.bw 472 | cp.mx = int(cp.h * w) 473 | cp.my = (-(cp.s * w) / 100) + w 474 | } 475 | -------------------------------------------------------------------------------- /src/image_resize.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import stbi 4 | import gx 5 | import os 6 | import iui.src.extra.file_dialog 7 | 8 | // wasm 9 | pub fn C.emscripten_run_script(&char) 10 | pub fn C.emscripten_run_script_string(&char) &char 11 | pub fn C.emscripten_sleep(int) 12 | 13 | fn (mut app App) open() { 14 | $if emscripten ? { 15 | unsafe { 16 | C.emscripten_run_script(c'iui.trigger = "openfiledialog"') 17 | app.need_open = true 18 | } 19 | return 20 | } 21 | 22 | path := file_dialog.open_dialog('Select Image File to Open') 23 | app.canvas.open(path) 24 | } 25 | 26 | fn cstr(the_string string) &char { 27 | return &char(the_string.str) 28 | } 29 | 30 | fn emsave(path string) { 31 | $if emscripten ? { 32 | C.emscripten_run_script(cstr('iui.trigger = "savefile=' + path + '"')) 33 | } 34 | } 35 | 36 | fn (mut this Image) open(path string) { 37 | png_file := stbi.load(path) or { panic(err) } 38 | 39 | this.data.file_name = path 40 | this.data.file_size = format_size(os.file_size(path)) 41 | this.load_stbi(png_file) 42 | } 43 | 44 | fn (mut app App) load_new(w int, h int) { 45 | png_file := make_stbi(w, h) 46 | app.canvas.load_stbi(png_file) 47 | } 48 | 49 | fn (mut this Image) resize(w int, h int) { 50 | png_file := stbi.resize_uint8(this.data.file, w, h) or { panic(err) } 51 | this.load_stbi(png_file) 52 | } 53 | 54 | fn (mut this Image) grayscale_filter() { 55 | mut change := Multichange.new() 56 | for x in 0 .. this.w { 57 | for y in 0 .. this.h { 58 | rgb := this.get(x, y) 59 | gray := (rgb.r + rgb.g + rgb.b) / 3 60 | new_color := gx.rgb(gray, gray, gray) 61 | this.set_raw(x, y, new_color, mut change) 62 | } 63 | } 64 | this.push(change) 65 | this.refresh() 66 | } 67 | 68 | fn (mut this Image) invert_filter() { 69 | mut change := Multichange.new() 70 | 71 | for x in 0 .. this.w { 72 | for y in 0 .. this.h { 73 | rgb := this.get(x, y) 74 | new_color := gx.rgba(255 - rgb.r, 255 - rgb.g, 255 - rgb.b, rgb.a) 75 | this.set_raw(x, y, new_color, mut change) 76 | } 77 | } 78 | this.push(change) 79 | this.refresh() 80 | } 81 | 82 | fn make_stbi(w int, h int) stbi.Image { 83 | img_size := w * h * 4 84 | img_pixels := unsafe { &u8(malloc(img_size)) } 85 | 86 | png_file := stbi.Image{ 87 | ok: true 88 | ext: 'png' 89 | data: img_pixels 90 | width: w 91 | height: h 92 | nr_channels: 4 93 | } 94 | return png_file 95 | } 96 | 97 | fn (mut this Image) load_stbi(png_file stbi.Image) { 98 | mut data := this.data 99 | this.zoom = 1 100 | data.file.free() 101 | data.file = png_file 102 | this.data = data 103 | this.img = data.id 104 | this.w = png_file.width 105 | this.h = png_file.height 106 | this.width = data.file.width 107 | this.height = data.file.height 108 | this.loaded = false 109 | } 110 | 111 | // TODO: Better upscale 112 | fn (mut this Image) upscale() { 113 | this.hq3x() 114 | // this.bilinear_interpolation(this.w * 2, this.h * 2) 115 | } 116 | 117 | fn (mut this Image) bilinear_interpolation(new_width int, new_height int) { 118 | src_width := this.w 119 | src_height := this.h 120 | 121 | mut en := make_stbi(new_width, new_height) 122 | 123 | data := &u8(this.data.file.data) 124 | 125 | for y in 0 .. new_height { 126 | for x in 0 .. new_width { 127 | // Calculate the position in the source image 128 | src_x := f32(x) * f32(src_width - 1) / f32(new_width - 1) 129 | src_y := f32(y) * f32(src_height - 1) / f32(new_height - 1) 130 | 131 | // Get the integer and fractional parts 132 | x0 := int(src_x) 133 | y0 := int(src_y) 134 | x1 := if x0 + 1 < src_width { x0 + 1 } else { x0 } 135 | y1 := if y0 + 1 < src_height { y0 + 1 } else { y0 } 136 | dx := src_x - f32(x0) 137 | dy := src_y - f32(y0) 138 | 139 | unsafe { 140 | a := data + (4 * (y0 * this.w + x0)) 141 | b := data + (4 * (y0 * this.w + x1)) 142 | c := data + (4 * (y1 * this.w + x0)) 143 | d := data + (4 * (y1 * this.w + x1)) 144 | 145 | // Perform the interpolation for each color component 146 | top_r := (1.0 - dx) * a[0] + dx * b[0] 147 | bottom_r := (1.0 - dx) * c[0] + dx * d[0] 148 | cr := u8((1.0 - dy) * top_r + dy * bottom_r) 149 | 150 | top_g := (1.0 - dx) * a[1] + dx * b[1] 151 | bottom_g := (1.0 - dx) * c[1] + dx * d[1] 152 | cg := u8((1.0 - dy) * top_g + dy * bottom_g) 153 | 154 | top_b := (1.0 - dx) * a[2] + dx * b[2] 155 | bottom_b := (1.0 - dx) * c[2] + dx * d[2] 156 | cb := u8((1.0 - dy) * top_b + dy * bottom_b) 157 | 158 | top_a := (1.0 - dx) * a[3] + dx * b[3] 159 | bottom_a := (1.0 - dx) * c[3] + dx * d[3] 160 | ca := u8((1.0 - dy) * top_a + dy * bottom_a) 161 | 162 | set_pixel(en, x, y, gx.rgba(cr, cg, cb, ca)) 163 | } 164 | } 165 | } 166 | 167 | this.load_stbi(en) 168 | } 169 | 170 | fn (mut this Image) scale2x() [][]gx.Color { 171 | src_width := this.w 172 | src_height := this.h 173 | mut dst := [][]gx.Color{len: src_height * 2, init: []gx.Color{len: src_width * 2}} 174 | 175 | mut en := make_stbi(this.w * 2, this.h * 2) 176 | 177 | for y in 0 .. src_height { 178 | for x in 0 .. src_width { 179 | c := this.get(x, y) 180 | a := if y > 0 { this.get(x, y - 1) } else { c } 181 | b := if x > 0 { this.get(x - 1, y) } else { c } 182 | d := if x < src_width - 1 { this.get(x + 1, y) } else { c } 183 | e := if y < src_height - 1 { this.get(x, y + 1) } else { c } 184 | 185 | dst[y * 2][x * 2] = if a == b { a } else { c } 186 | dst[y * 2][x * 2 + 1] = if a == d { a } else { c } 187 | dst[y * 2 + 1][x * 2] = if e == b { e } else { c } 188 | dst[y * 2 + 1][x * 2 + 1] = if e == d { e } else { c } 189 | } 190 | } 191 | 192 | for x in 0 .. this.w * 2 { 193 | for y in 0 .. this.h * 2 { 194 | set_pixel(en, x, y, dst[y][x]) 195 | } 196 | } 197 | this.load_stbi(en) 198 | 199 | return dst 200 | } 201 | 202 | fn (mut this Image) hq3x() { 203 | src_width := this.w 204 | src_height := this.h 205 | 206 | mut en := make_stbi(this.w * 3, this.h * 3) 207 | 208 | for y in 0 .. src_height { 209 | for x in 0 .. src_width { 210 | c := this.get(x, y) 211 | a := if y > 0 { this.get(x, y - 1) } else { c } 212 | b := if x > 0 { this.get(x - 1, y) } else { c } 213 | d := if x < src_width - 1 { this.get(x + 1, y) } else { c } 214 | e := if y < src_height - 1 { this.get(x, y + 1) } else { c } 215 | 216 | // Fill the 3x3 block 217 | for dy in 0 .. 3 { 218 | for dx in 0 .. 3 { 219 | set_pixel(en, x * 3 + dx, y * 3 + dy, c) 220 | } 221 | } 222 | 223 | // Apply the hq3x rules 224 | if a == b && a != d && b != e { 225 | set_pixel(en, x * 3, y * 3, a) 226 | } 227 | if a == d && a != b && d != e { 228 | set_pixel(en, x * 3 + 2, y * 3, d) 229 | } 230 | if e == b && e != a && b != d { 231 | set_pixel(en, x * 3, y * 3 + 2, e) 232 | } 233 | if e == d && e != a && d != b { 234 | set_pixel(en, x * 3 + 2, y * 3 + 2, e) 235 | } 236 | } 237 | } 238 | 239 | this.load_stbi(en) 240 | } 241 | 242 | fn (mut this Image) increase_alpha() { 243 | for x in 0 .. this.w { 244 | for y in 0 .. this.h { 245 | color := this.get(x, y) 246 | if color.a < 5 { 247 | continue 248 | } 249 | 250 | new_color := gx.rgba(color.r, color.g, color.b, color.a + 5) 251 | this.set(x, y, new_color) 252 | } 253 | } 254 | this.refresh() 255 | } 256 | -------------------------------------------------------------------------------- /src/image_save.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import stbi 5 | import os 6 | 7 | fn (app &App) save() { 8 | mut this := app.data 9 | 10 | if this.file_name.ends_with('jpg') { 11 | app.write_jpg(this.file, this.file_name) 12 | } else { 13 | app.write_img(this.file, this.file_name) 14 | } 15 | 16 | file_size := format_size(os.file_size(this.file_name)) 17 | this.file_size = file_size 18 | emsave(this.file_name) 19 | } 20 | 21 | fn responsive_field(mut e ui.DrawEvent) { 22 | padding := 20 23 | tw := e.ctx.text_width(e.target.text) + padding 24 | 25 | ws := e.ctx.gg.window_size().width 26 | inw := if ws < 500 { ws } else { 500 } 27 | min := inw / 3 28 | e.target.width = if tw > min { tw } else { min } 29 | } 30 | 31 | fn responsive_modal(mut e ui.DrawEvent) { 32 | mut tar := e.get_target[ui.Modal]() 33 | ws := e.ctx.gg.window_size().width 34 | 35 | inw := if ws < 500 { ws } else { 500 } 36 | nnw := inw - 10 37 | if tar.in_width == nnw { 38 | return 39 | } 40 | tar.in_width = nnw 41 | tar.children[0].width = nnw - 1 42 | tar.children[0].height = tar.in_height 43 | tar.children[0].children[1].width = nnw 44 | } 45 | 46 | fn responsive_modal_panel(mut e ui.DrawEvent) { 47 | } 48 | 49 | fn (app &App) save_as() { 50 | mut data := app.data 51 | 52 | mut modal := ui.Modal.new( 53 | title: 'Save As..' 54 | ) 55 | 56 | modal.subscribe_event('draw', responsive_modal) 57 | 58 | modal.top_off = 5 59 | modal.in_height = 280 60 | w := modal.in_width - 10 61 | 62 | mut p := ui.Panel.new( 63 | layout: ui.BoxLayout.new( 64 | ori: 1 65 | ) 66 | ) 67 | // p.set_bounds(4, 0, w, modal.in_height) 68 | 69 | folder := os.dir(data.file_name) 70 | file_name := os.file_name(data.file_name) 71 | 72 | mut f := ui.TextField.new( 73 | text: folder 74 | ) 75 | f.set_bounds(0, 0, w / 2, 30) 76 | f.subscribe_event('draw', responsive_field) 77 | 78 | mut cb := ui.Selectbox.new( 79 | text: os.file_ext(file_name)[1..].to_upper() 80 | items: ['PNG', 'JPG', 'BMP', 'TGA'] 81 | ) 82 | 83 | mut nam := ui.TextField.new( 84 | text: file_name 85 | ) 86 | 87 | cb.set_bounds(0, 0, 140, 30) 88 | cb.subscribe_event('item_change', fn [mut nam] (mut e ui.ItemChangeEvent) { 89 | txt := e.target.text 90 | if !nam.text.ends_with(txt) { 91 | old_type := os.file_ext(nam.text) 92 | nam.text = nam.text.replace(old_type, '.' + txt.to_lower()) 93 | } 94 | }) 95 | 96 | mut tb_f := ui.SettingsCard.new(text: 'Save Folder', description: 'The Directory to save in') 97 | tb_f.stretch = true 98 | tb_f.add_child(f) 99 | 100 | nam.set_bounds(0, 0, w - 210, 30) 101 | 102 | mut tb_fn := ui.Titlebox.new( 103 | text: 'File Name' 104 | children: [nam] 105 | ) 106 | 107 | mut tb_cb := ui.Titlebox.new( 108 | text: 'Save as Type' 109 | children: [cb] 110 | ) 111 | 112 | mut p2 := ui.Panel.new(layout: ui.FlowLayout.new()) 113 | p2.subscribe_event('draw', responsive_modal_panel) 114 | 115 | p2.set_bounds(0, 0, w, 170) 116 | p2.add_child(tb_fn) 117 | p2.add_child(tb_cb) 118 | 119 | p.add_child(tb_f) 120 | p.add_child(p2) 121 | modal.add_child(p) 122 | 123 | modal.needs_init = false 124 | 125 | mut close := ui.Button.new(text: 'Save') 126 | modal.add_child(close) 127 | 128 | close.subscribe_event('mouse_up', fn [app, mut data, mut f, mut nam] (mut e ui.MouseEvent) { 129 | full_path := os.join_path(f.text, nam.text) 130 | typ := os.file_ext(full_path).to_lower() 131 | 132 | ui.default_modal_close_fn(mut e) 133 | 134 | mut good := false 135 | if typ == '.png' { 136 | good = app.write_img(data.file, full_path) 137 | } 138 | if typ == '.jpg' { 139 | good = app.write_jpg(data.file, full_path) 140 | } 141 | if typ == '.bmp' { 142 | good = app.write_bmp(data.file, full_path) 143 | } 144 | if typ == '.tga' { 145 | good = app.write_tga(data.file, full_path) 146 | } 147 | if typ == '.svg' { 148 | // TOOD 149 | } 150 | if good { 151 | data.file_name = full_path 152 | file_size := format_size(os.file_size(full_path)) 153 | data.file_size = file_size 154 | emsave(data.file_name) 155 | } 156 | }) 157 | 158 | mut can := modal.make_close_btn(true) 159 | can.text = 'Cancel' 160 | 161 | y := modal.in_height - 40 // 250 162 | close.set_bounds(w - 280, y, 135, 30) 163 | can.set_bounds(w - 140, y, 80, 30) 164 | 165 | app.win.add_child(modal) 166 | } 167 | 168 | fn word_wrap_a(txt string, max int) string { 169 | mut words := txt.split(' ') 170 | mut line_len := 0 171 | mut output := '' 172 | 173 | for word in words { 174 | if line_len + word.len > max { 175 | output += '\n${word} ' 176 | line_len = word.len + 1 177 | } else { 178 | output += '${word} ' 179 | line_len += word.len + 1 180 | } 181 | } 182 | return output 183 | } 184 | 185 | fn (app &App) show_error(title string, msg IError) { 186 | mut modal := ui.Modal.new( 187 | title: title 188 | ) 189 | modal.top_off = 20 190 | modal.in_height = 110 191 | text := msg.msg() 192 | mut txt := ui.Label.new( 193 | text: 'Error Code: ${msg.code()}; Message:\n${text}' 194 | ) 195 | txt.set_pos(8, 8) 196 | txt.pack() 197 | modal.add_child(txt) 198 | app.win.add_child(modal) 199 | } 200 | 201 | // Write as PNG 202 | pub fn (app &App) write_img(img stbi.Image, path string) bool { 203 | stbi.stbi_write_png(path, img.width, img.height, 4, img.data, img.width * 4) or { 204 | app.show_error('stbi_image save error', err) 205 | return false 206 | } 207 | return true 208 | } 209 | 210 | // Write as JPG 211 | pub fn (app &App) write_jpg(img stbi.Image, path string) bool { 212 | stbi.stbi_write_jpg(path, img.width, img.height, 4, img.data, 80) or { 213 | app.show_error('stbi_image save error', err) 214 | return false 215 | } 216 | return true 217 | } 218 | 219 | // Write as Bitmap 220 | pub fn (app &App) write_bmp(img stbi.Image, path string) bool { 221 | stbi.stbi_write_bmp(path, img.width, img.height, 4, img.data) or { 222 | app.show_error('stbi_image save error', err) 223 | return false 224 | } 225 | return true 226 | } 227 | 228 | // Write as TGA 229 | pub fn (app &App) write_tga(img stbi.Image, path string) bool { 230 | stbi.stbi_write_tga(path, img.width, img.height, 4, img.data) or { 231 | app.show_error('stbi_image save error', err) 232 | return false 233 | } 234 | return true 235 | } 236 | -------------------------------------------------------------------------------- /src/image_view.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import stbi 4 | import gx 5 | import iui as ui 6 | import gg 7 | import os 8 | 9 | @[heap] 10 | struct ImageViewData { 11 | mut: 12 | file stbi.Image 13 | id int 14 | file_name string 15 | file_size string 16 | } 17 | 18 | pub fn (mut app App) make_image_view(file string) &ui.Panel { 19 | mut p := ui.Panel.new( 20 | layout: ui.FlowLayout.new( 21 | hgap: 0 22 | vgap: 0 23 | ) 24 | ) 25 | 26 | mut png_file := stbi.load(file) or { make_stbi(0, 0) } 27 | 28 | mut data := &ImageViewData{ 29 | file: png_file 30 | file_name: file 31 | } 32 | app.data = data 33 | 34 | mut img := image_from_data(data) 35 | img.app = app 36 | app.canvas = img 37 | p.add_child(img) 38 | 39 | p.subscribe_event('after_draw', img_panel_draw) 40 | 41 | if os.exists(file) { 42 | file_size := format_size(os.file_size(file)) 43 | data.file_size = file_size 44 | } 45 | 46 | p.set_pos(24, 24) 47 | 48 | if app.data.file_size.len == 0 { 49 | app.load_new(32, 32) 50 | } 51 | 52 | return p 53 | } 54 | 55 | fn img_panel_draw(mut e ui.DrawEvent) { 56 | mut app := e.ctx.win.get[&App]('app') 57 | e.target.width = app.canvas.width + 2 58 | e.target.height = app.canvas.height + 2 59 | 60 | // e.ctx.gg.draw_rect_filled(e.target.x, e.target.y, e.target.width, e.target.height, e.ctx.theme.accent_fill) 61 | 62 | if app.need_open { 63 | $if emscripten ? { 64 | if C.emscripten_run_script_string(c'iui.task_result').vstring() == '1' { 65 | C.emscripten_run_script(c'iui.task_result = "0"') 66 | vall := C.emscripten_run_script_string(c'iui.latest_file.name').vstring() 67 | app.canvas.open(vall) 68 | app.need_open = false 69 | } 70 | } 71 | } 72 | } 73 | 74 | fn (mut img Image) set_zoom(mult f32) { 75 | img.width = int(img.w * mult) 76 | img.height = int(img.h * mult) 77 | img.zoom = mult 78 | 79 | mut sm := f32(128.0) 80 | 81 | zm := mult 82 | if zm > 10 { 83 | sf := zm / 8 84 | sm = (sf * 128) / 4 85 | } 86 | 87 | img.bw = int(img.width / sm) 88 | img.bh = int(img.height / sm) 89 | } 90 | 91 | fn (mut img Image) get_zoom() f32 { 92 | return img.zoom 93 | } 94 | 95 | fn format_size(val f64) string { 96 | by := f64(1024) 97 | 98 | kb := val / by 99 | str := '${kb}'.str()[0..4] 100 | 101 | if kb > 1024 { 102 | mb := kb / by 103 | str2 := mb.str()[0..4] 104 | 105 | return str + ' KB / ${str2} MB' 106 | } 107 | return str + ' KB' 108 | } 109 | 110 | fn make_gg_image(mut storage ImageViewData, mut win ui.Window, first bool) { 111 | if first { 112 | storage.id = win.gg.new_streaming_image(storage.file.width, storage.file.height, 113 | 4, gg.StreamingImageConfig{ 114 | pixel_format: .rgba8 115 | mag_filter: .nearest 116 | }) 117 | } 118 | win.gg.update_pixel_data(storage.id, storage.file.data) 119 | } 120 | 121 | // Get RGB value from image loaded with STBI 122 | pub fn get_pixel(x int, y int, this stbi.Image) gx.Color { 123 | if x == -1 || y == -1 { 124 | return gx.rgba(0, 0, 0, 0) 125 | } 126 | 127 | x_oob := x < 0 || x >= this.width 128 | y_oob := y < 0 || y >= this.height 129 | if x_oob || y_oob { 130 | return gx.rgba(0, 0, 0, 0) 131 | } 132 | 133 | unsafe { 134 | data := &u8(this.data) 135 | p := data + (4 * (y * this.width + x)) 136 | r := p[0] 137 | g := p[1] 138 | b := p[2] 139 | a := p[3] 140 | return gx.Color{r, g, b, a} 141 | } 142 | } 143 | 144 | fn mix_color(ca gx.Color, cb gx.Color) gx.Color { 145 | if cb.a < 0 { 146 | return ca 147 | } 148 | 149 | ratio := f32(.5) 150 | r := u8(ca.r * ratio) + u8(cb.r * ratio) 151 | g := u8(ca.g * ratio) + u8(cb.g * ratio) 152 | b := u8(ca.b * ratio) + u8(cb.b * ratio) 153 | a := u8(ca.a * ratio) + u8(cb.a * ratio) 154 | return gx.rgba(r, g, b, a) 155 | } 156 | 157 | type Changes = Change | Multichange 158 | 159 | struct Change { 160 | x int 161 | y int 162 | from gx.Color 163 | to gx.Color 164 | mut: 165 | batch bool 166 | } 167 | 168 | fn Multichange.new() Multichange { 169 | return Multichange{} 170 | } 171 | 172 | fn (mut mc Multichange) change_at(x int, y int, a gx.Color, b gx.Color) { 173 | mc.changes << Change{ 174 | x: x 175 | y: y 176 | from: a 177 | to: b 178 | } 179 | } 180 | 181 | fn (mut i Image) push(change Multichange) { 182 | i.history.insert(0, change) 183 | } 184 | 185 | struct Multichange { 186 | Change 187 | mut: 188 | changes []Change 189 | } 190 | 191 | fn (this Changes) compare(b Changes) u8 { 192 | if this == b { 193 | return 3 194 | } 195 | 196 | same_pos := this.x == b.x && this.y == b.y 197 | same_from := this.from == b.from 198 | same_to := this.to == b.to 199 | 200 | if same_pos && same_to && same_from { 201 | return 2 202 | } 203 | if same_pos && same_to { 204 | return 1 205 | } 206 | return 0 207 | } 208 | 209 | @[deprecated] 210 | fn (mut this Image) note_multichange() { 211 | change := Change{ 212 | x: -1 213 | y: -1 214 | from: gx.white 215 | to: gx.white 216 | batch: false 217 | } 218 | this.history.insert(0, change) 219 | } 220 | 221 | fn (mut this Image) undo() { 222 | if this.history.len == 0 { 223 | return 224 | } 225 | last_change := this.history[0] 226 | 227 | if last_change is Multichange { 228 | for change in last_change.changes { 229 | set_pixel(this.data.file, change.x, change.y, change.from) 230 | } 231 | this.history.delete(0) 232 | if last_change.batch { 233 | this.undo() 234 | return 235 | } 236 | 237 | this.refresh() 238 | return 239 | } 240 | 241 | old_color := last_change.from 242 | x := last_change.x 243 | y := last_change.y 244 | batch := last_change.batch 245 | 246 | set_pixel(this.data.file, x, y, old_color) 247 | this.history.delete(0) 248 | 249 | if batch { 250 | mut b := true 251 | for b { 252 | change := this.history[0] 253 | set_pixel(this.data.file, change.x, change.y, change.from) 254 | b = change.batch 255 | this.history.delete(0) 256 | if change.x == -1 { 257 | break 258 | } 259 | } 260 | } 261 | 262 | this.refresh() 263 | } 264 | 265 | @[deprecated] 266 | fn (mut this Image) mark_batch_change() { 267 | this.history[0].batch = true 268 | } 269 | 270 | fn (mut this Image) set(x int, y int, color gx.Color) bool { 271 | return this.set2(x, y, color, false) 272 | } 273 | 274 | fn (mut this Image) set2(x int, y int, color gx.Color, batch bool) bool { 275 | if x < 0 || y < 0 || x >= this.w || y >= this.h { 276 | // dump('${x} ${y}') 277 | return false 278 | } 279 | 280 | from := this.get(x, y) 281 | if from == color { 282 | return true 283 | } 284 | 285 | // dump('debug: set2 ${this.history.len}') 286 | 287 | change := Change{ 288 | x: x 289 | y: y 290 | from: from 291 | to: color 292 | batch: batch 293 | } 294 | 295 | if this.history.len > 1000 { 296 | this.history.delete_last() 297 | } 298 | 299 | if this.history.len > 0 { 300 | if this.history[0].compare(change) == 0 { 301 | this.history.insert(0, change) 302 | } 303 | } else { 304 | this.history.insert(0, change) 305 | } 306 | 307 | set_pix(this.data.file, x, y, color) 308 | return true 309 | } 310 | 311 | fn (mut this Image) set_raw(x int, y int, color gx.Color, mut ch Multichange) bool { 312 | from := this.get(x, y) 313 | if from == color { 314 | return true 315 | } 316 | 317 | ch.change_at(x, y, from, color) 318 | return set_pixel(this.data.file, x, y, color) 319 | } 320 | 321 | fn (mut this Image) set_no_undo(x int, y int, color gx.Color) bool { 322 | return set_pixel(this.data.file, x, y, color) 323 | } 324 | 325 | fn (mut this Image) get(x int, y int) gx.Color { 326 | return get_pixel(x, y, this.data.file) 327 | } 328 | 329 | fn (mut this Image) refresh() { 330 | // mut data := this.data 331 | // refresh_img(mut data, mut this.app.win.gg) 332 | this.app.win.gg.update_pixel_data(this.data.id, this.data.file.data) 333 | } 334 | 335 | // Get RGB value from image loaded with STBI 336 | fn set_pixel(image stbi.Image, x int, y int, color gx.Color) bool { 337 | if x < 0 || x >= image.width { 338 | return false 339 | } 340 | 341 | if y < 0 || y >= image.height { 342 | return false 343 | } 344 | 345 | unsafe { 346 | data := &u8(image.data) 347 | p := data + (4 * (y * image.width + x)) 348 | p[0] = color.r 349 | p[1] = color.g 350 | p[2] = color.b 351 | p[3] = color.a 352 | return true 353 | } 354 | } 355 | 356 | fn set_pix(image stbi.Image, x int, y int, color gx.Color) { 357 | unsafe { 358 | data := &u8(image.data) 359 | p := data + (4 * (y * image.width + x)) 360 | p[0] = color.r 361 | p[1] = color.g 362 | p[2] = color.b 363 | p[3] = color.a 364 | } 365 | } 366 | 367 | // IMAGE 368 | 369 | // Image - implements Component interface 370 | pub struct Image { 371 | ui.Component_A 372 | pub mut: 373 | app &App = unsafe { nil } 374 | data &ImageViewData = unsafe { nil } 375 | w int 376 | h int 377 | sx f32 378 | sy f32 379 | mx int 380 | my int 381 | img int 382 | zoom f32 383 | loaded bool 384 | history []Changes 385 | history_index int 386 | last_x int = -1 387 | last_y int 388 | bw int 389 | bh int 390 | } 391 | 392 | pub fn image_from_data(data &ImageViewData) &Image { 393 | return &Image{ 394 | data: data 395 | img: data.id 396 | w: data.file.width 397 | h: data.file.height 398 | width: data.file.width 399 | height: data.file.height 400 | zoom: 1 401 | } 402 | } 403 | 404 | // Load image on first drawn frame 405 | pub fn (mut this Image) load_if_not_loaded(ctx &ui.GraphicsContext) { 406 | mut win := ctx.win 407 | 408 | make_gg_image(mut this.data, mut win, true) 409 | this.img = this.data.id 410 | canvas_height := this.app.sv.height 411 | zoom_fit := canvas_height / this.data.file.height 412 | if zoom_fit > 1 { 413 | this.set_zoom(zoom_fit - 1) 414 | } 415 | this.loaded = true 416 | } 417 | 418 | pub fn (mut this Image) draw(ctx &ui.GraphicsContext) { 419 | if !this.loaded { 420 | this.load_if_not_loaded(ctx) 421 | } 422 | 423 | ctx.gg.draw_image_with_config(gg.DrawImageConfig{ 424 | img_id: this.app.bg_id 425 | img_rect: gg.Rect{ 426 | x: this.x 427 | y: this.y 428 | width: this.width 429 | height: this.height 430 | } 431 | }) 432 | 433 | ctx.gg.draw_image_with_config(gg.DrawImageConfig{ 434 | img_id: this.img 435 | img_rect: gg.Rect{ 436 | x: this.x 437 | y: this.y 438 | width: this.width 439 | height: this.height 440 | } 441 | }) 442 | 443 | color := ctx.theme.text_color 444 | ctx.gg.draw_rect_empty(this.x, this.y, this.width, this.height, color) 445 | 446 | // Find mouse location data 447 | this.calculate_mouse_pixel(ctx) 448 | 449 | // Gridlines 450 | if this.app.settings.show_gridlines { 451 | a := ctx.theme.accent_fill 452 | c := gx.rgba(a.r, a.g, a.b, 50) 453 | 454 | for x in 0 .. this.w { 455 | ctx.gg.draw_line(this.x + (x * this.zoom), this.y, this.x + (x * this.zoom), 456 | this.y + this.height, c) 457 | } 458 | 459 | for y in 0 .. this.h { 460 | ctx.gg.draw_line(this.x, this.y + (y * this.zoom), this.x + this.width, this.y + 461 | (y * this.zoom), c) 462 | } 463 | } 464 | 465 | // Tools 466 | // TODO: note we need to do this for our parent too, 467 | // so we can catch outside mouse up events. 468 | 469 | mut tool := this.app.tool 470 | tool.draw_hover_fn(this, ctx) 471 | 472 | if this.is_mouse_down { 473 | if ctx.win.bar.tik > 90 { 474 | tool.draw_down_fn(this, ctx) 475 | } 476 | } 477 | 478 | if this.is_mouse_rele { 479 | if ctx.win.bar.tik > 90 { 480 | tool.draw_click_fn(this, ctx) 481 | } 482 | this.is_mouse_rele = false 483 | } 484 | 485 | if !this.is_mouse_down { 486 | if this.last_x != -1 { 487 | this.last_x = -1 488 | this.last_y = -1 489 | } 490 | } 491 | } 492 | 493 | // Updates which pixel the mouse is located 494 | pub fn (mut this Image) calculate_mouse_pixel(ctx &ui.GraphicsContext) { 495 | mx := ctx.win.mouse_x - this.x 496 | my := ctx.win.mouse_y - this.y 497 | 498 | ix := int(mx / this.zoom) 499 | sx := this.x + (ix * this.zoom) 500 | 501 | iy := int(my / this.zoom) 502 | sy := this.y + (iy * this.zoom) 503 | 504 | if my > 0 { 505 | if my > this.height { 506 | nsy := this.y + ((this.h - 1) * this.zoom) 507 | this.sy = nsy 508 | this.my = this.h - 1 509 | } else { 510 | this.sy = sy 511 | this.my = iy 512 | } 513 | } else { 514 | this.sy = this.y 515 | this.my = 0 516 | } 517 | 518 | if mx > ((this.w - 1) * this.zoom) { 519 | nsx := this.x + ((this.w - 1) * this.zoom) 520 | 521 | this.sx = nsx 522 | this.mx = this.w - 1 523 | return 524 | } 525 | 526 | if mx < 0 { 527 | this.sx = this.x 528 | this.mx = 0 529 | return 530 | } 531 | 532 | this.sx = sx 533 | this.mx = ix 534 | } 535 | 536 | fn (this &Image) get_point_screen_pos(x int, y int) (f32, f32) { 537 | sx := this.x + (x * this.zoom) 538 | sy := this.y + (y * this.zoom) 539 | return sx, sy 540 | } 541 | 542 | fn (this &Image) get_pos_point(x f32, y f32) (int, int) { 543 | px := (x - this.x) / this.zoom 544 | py := (y - this.y) / this.zoom 545 | return int(px), int(py) 546 | } 547 | 548 | fn refresh_img(mut storage ImageViewData, mut ctx gg.Context) { 549 | ctx.update_pixel_data(storage.id, storage.file.data) 550 | } 551 | 552 | fn (mut img Image) set_line(x1 int, y1 int, x2 int, y2 int, c gx.Color, size int, mut change Multichange) { 553 | dx := abs(x2 - x1) 554 | dy := abs(y2 - y1) 555 | sx := if x1 < x2 { 1 } else { -1 } 556 | sy := if y1 < y2 { 1 } else { -1 } 557 | mut err := dx - dy 558 | 559 | mut x := x1 560 | mut y := y1 561 | 562 | no_round := !img.app.settings.round_ends 563 | 564 | for { 565 | if size == 1 { 566 | img.set_raw(x, y, c, mut change) 567 | } else { 568 | for i in -size / 2 .. size / 2 { 569 | for j in -size / 2 .. size / 2 { 570 | if i * i + j * j <= (size / 2) * (size / 2) || no_round { 571 | img.set_raw(x + i, y + j, c, mut change) 572 | } 573 | } 574 | } 575 | } 576 | if x == x2 && y == y2 { 577 | break 578 | } 579 | e2 := 2 * err 580 | if e2 > -dy { 581 | err -= dy 582 | x += sx 583 | } 584 | if e2 < dx { 585 | err += dx 586 | y += sy 587 | } 588 | } 589 | 590 | // Draw rounded edges 591 | if !no_round { 592 | draw_circle_filled(mut img, x1, y1, size / 2, c, mut change) 593 | draw_circle_filled(mut img, x2, y2, size / 2, c, mut change) 594 | } 595 | } 596 | 597 | fn draw_circle_filled(mut img &Image, x int, y int, radius int, c gx.Color, mut change Multichange) { 598 | for i in -radius .. radius { 599 | for j in -radius .. radius { 600 | if i * i + j * j <= radius * radius { 601 | img.set_raw(x + i, y + j, c, mut change) 602 | } 603 | } 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /src/menubar.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gx 5 | 6 | fn upscale_click(mut win ui.Window, com ui.MenuItem) { 7 | mut app := win.get[&App]('app') 8 | 9 | txt := com.text 10 | 11 | if txt.contains('bilinear') { 12 | app.canvas.bilinear_interpolation(app.canvas.w * 2, app.canvas.h * 2) 13 | return 14 | } 15 | 16 | if txt.contains('scale2x') { 17 | app.canvas.scale2x() 18 | return 19 | } 20 | 21 | if txt.contains('hq3x') { 22 | app.canvas.hq3x() 23 | return 24 | } 25 | 26 | app.canvas.upscale() 27 | } 28 | 29 | fn tool_item_click(mut win ui.Window, com ui.MenuItem) { 30 | mut app := win.get[&App]('app') 31 | 32 | if com.text == 'CustomPencil' { 33 | if app.tool.tool_name != 'Custom Pencil' { 34 | app.tool = &CustomPencilTool{} 35 | } 36 | app.show_custom_pencil_modal() 37 | } else { 38 | app.set_tool_by_name(com.text) 39 | } 40 | 41 | // "Fake" a press 42 | for mut btn in app.sidebar.children[0].children { 43 | btn.is_selected = btn.text == com.text 44 | } 45 | } 46 | 47 | fn grayscale_click(mut win ui.Window, com ui.MenuItem) { 48 | mut app := win.get[&App]('app') 49 | app.canvas.grayscale_filter() 50 | } 51 | 52 | fn inc_alpha_click(mut win ui.Window, com ui.MenuItem) { 53 | mut app := win.get[&App]('app') 54 | app.canvas.increase_alpha() 55 | } 56 | 57 | fn invert_click(mut win ui.Window, com ui.MenuItem) { 58 | mut app := win.get[&App]('app') 59 | app.canvas.invert_filter() 60 | } 61 | 62 | fn undo_click(mut win ui.Window, com ui.MenuItem) { 63 | mut app := win.get[&App]('app') 64 | app.canvas.undo() 65 | } 66 | 67 | fn new_click(mut win ui.Window, com ui.MenuItem) { 68 | mut app := win.get[&App]('app') 69 | // app.load_new(1024, 1024) 70 | 71 | app.show_new_modal(1024, 1024) 72 | } 73 | 74 | fn open_click(mut win ui.Window, com ui.MenuItem) { 75 | mut app := win.get[&App]('app') 76 | app.open() 77 | } 78 | 79 | fn save_click(mut win ui.Window, com ui.MenuItem) { 80 | mut app := win.get[&App]('app') 81 | app.save() 82 | } 83 | 84 | fn save_as_click(mut win ui.Window, com ui.MenuItem) { 85 | mut app := win.get[&App]('app') 86 | app.save_as() 87 | } 88 | 89 | fn menu_zoom_out_click(mut win ui.Window, com ui.MenuItem) { 90 | mut app := win.get[&App]('app') 91 | nz := app.canvas.zoom - 1 92 | app.canvas.set_zoom(nz) 93 | } 94 | 95 | fn menu_zoom_in_click(mut win ui.Window, com ui.MenuItem) { 96 | mut app := win.get[&App]('app') 97 | nz := app.canvas.zoom + 1 98 | app.canvas.set_zoom(nz) 99 | } 100 | 101 | fn img_prop_item_click(mut e ui.MouseEvent) { 102 | mut app := e.ctx.win.get[&App]('app') 103 | app.show_prop_modal(e.ctx) 104 | } 105 | 106 | // Make menubar 107 | fn (mut app App) make_menubar(mut window ui.Window) { 108 | // Setup Menubar and items 109 | window.bar = ui.Menubar.new() 110 | window.bar.set_animate(true) 111 | 112 | // Win11 MSPaint has 7px padding on menu bar 113 | window.bar.set_padding(8) 114 | 115 | // Add MenuItems 116 | window.bar.add_child(make_file_menu()) 117 | window.bar.add_child(make_edit_menu()) 118 | window.bar.add_child(make_view_menu()) 119 | window.bar.add_child(make_tool_menu()) 120 | window.bar.add_child(make_shape_menu()) 121 | 122 | window.bar.add_child(ui.MenuItem.new( 123 | text: 'Size' 124 | children: [ 125 | size_menu_item(1), 126 | size_menu_item(2), 127 | size_menu_item(4), 128 | size_menu_item(8), 129 | size_menu_item(16), 130 | size_menu_item(32), 131 | size_menu_item(64), 132 | ui.MenuItem.new( 133 | text: 'Custom' 134 | click_event_fn: menu_size_custom_click 135 | ), 136 | ] 137 | )) 138 | 139 | mut theme_menu := ui.MenuItem.new( 140 | text: 'Theme' 141 | ) 142 | mut themes := ui.get_all_themes() 143 | for theme2 in themes { 144 | mut item := ui.MenuItem.new(text: theme2.name) 145 | item.set_click(theme_click) 146 | theme_menu.add_child(item) 147 | } 148 | 149 | window.bar.add_child(theme_menu) 150 | 151 | // undo_img := $embed_file('assets/undo.png') 152 | // undo_icon := ui.image_from_bytes(mut window, undo_img.to_bytes(), 24, 24) 153 | 154 | mut undo_item := ui.MenuItem.new( 155 | click_event_fn: undo_click 156 | uicon: '\ue966' 157 | ) 158 | undo_item.width = 30 159 | window.bar.add_child(undo_item) 160 | } 161 | 162 | // File Item 163 | fn make_file_menu() &ui.MenuItem { 164 | item := ui.MenuItem.new( 165 | text: 'File' 166 | children: [ 167 | ui.MenuItem.new( 168 | text: 'New' 169 | click_event_fn: new_click 170 | uicon: '\ue130' 171 | ), 172 | ui.MenuItem.new( 173 | text: 'Open...' 174 | click_event_fn: open_click 175 | uicon: '\ue838' 176 | ), 177 | ui.MenuItem.new( 178 | text: 'Save' 179 | click_event_fn: save_click 180 | uicon: '\ue74e' 181 | ), 182 | ui.MenuItem.new( 183 | text: 'Save As...' 184 | click_event_fn: save_as_click 185 | uicon: '\ue792' 186 | ), 187 | ui.MenuItem.new( 188 | text: 'Image Properties' 189 | click_fn: img_prop_item_click 190 | uicon: '\uE90E' 191 | ), 192 | ui.MenuItem.new( 193 | text: 'Settings' 194 | click_event_fn: settings_click 195 | uicon: '\ue995' 196 | ), 197 | ui.MenuItem.new( 198 | text: 'About Paint' 199 | click_event_fn: about_click 200 | uicon: '\ue949' 201 | ), 202 | ui.MenuItem.new( 203 | text: 'About iUI' 204 | uicon: '\ue949' 205 | ), 206 | ] 207 | ) 208 | return item 209 | } 210 | 211 | // Edit Item 212 | fn make_edit_menu() &ui.MenuItem { 213 | item := ui.MenuItem.new( 214 | text: 'Edit' 215 | children: [ 216 | ui.MenuItem.new( 217 | text: 'Upscale 2x' 218 | click_event_fn: upscale_click 219 | ), 220 | ui.MenuItem.new( 221 | text: 'Scaling...' 222 | children: [ 223 | ui.MenuItem.new( 224 | text: 'bilinear interpolation' 225 | click_event_fn: upscale_click 226 | ), 227 | ui.MenuItem.new( 228 | text: 'scale2x' 229 | click_event_fn: upscale_click 230 | ), 231 | ui.MenuItem.new( 232 | text: 'hq3x' 233 | click_event_fn: upscale_click 234 | ), 235 | ] 236 | ), 237 | ui.MenuItem.new( 238 | text: 'Apply Grayscale' 239 | click_event_fn: grayscale_click 240 | ), 241 | ui.MenuItem.new( 242 | text: 'Invert Image' 243 | click_event_fn: invert_click 244 | ), 245 | ui.MenuItem.new( 246 | text: 'Increase Alpha' 247 | click_event_fn: inc_alpha_click 248 | ), 249 | ui.MenuItem.new( 250 | uicon: '\ue966' 251 | text: 'Undo' 252 | click_event_fn: undo_click 253 | ), 254 | ui.MenuItem.new( 255 | uicon: '\uea58' 256 | text: 'Resize Canvas' 257 | click_event_fn: menu_resize_click 258 | ), 259 | ] 260 | ) 261 | return item 262 | } 263 | 264 | // Tools 265 | fn make_tool_menu() &ui.MenuItem { 266 | mut tool_item := ui.MenuItem.new( 267 | text: 'Tools' 268 | ) 269 | 270 | labels := ['Pencil', 'Fill', 'Drag', 'Select', 'Airbrush', 'Dropper', 'WidePencil', 271 | 'CustomPencil'] 272 | uicons := ['\uED63', '\ue90c', '\uf047', '\ue003', '\uec5a', '\ue90b', '\uED63', '', ''] 273 | 274 | for i, label in labels { 275 | tool_item.add_child(ui.MenuItem.new( 276 | text: label 277 | click_event_fn: tool_item_click 278 | uicon: uicons[i] 279 | )) 280 | } 281 | return tool_item 282 | } 283 | 284 | // Shapes 285 | fn make_shape_menu() &ui.MenuItem { 286 | mut item := ui.MenuItem.new( 287 | text: 'Shapes' 288 | ) 289 | 290 | for i, label in shape_labels { 291 | item.add_child(ui.MenuItem.new( 292 | text: label 293 | click_event_fn: tool_item_click 294 | uicon: shape_uicons[i] 295 | )) 296 | } 297 | return item 298 | } 299 | 300 | // View Menu 301 | fn make_view_menu() &ui.MenuItem { 302 | return ui.MenuItem.new( 303 | text: 'View' 304 | children: [ 305 | ui.MenuItem.new( 306 | text: 'Fit Canvas' 307 | click_event_fn: menubar_fit_zoom_click 308 | uicon: '\uf002' 309 | ), 310 | ui.MenuItem.new( 311 | text: 'Zoom-out' 312 | uicon: '\ue989' 313 | click_event_fn: menu_zoom_out_click 314 | ), 315 | ui.MenuItem.new( 316 | text: 'Zoom-In' 317 | click_event_fn: menu_zoom_in_click 318 | uicon: '\ue988' 319 | ), 320 | ui.MenuItem.new( 321 | text: 'Gridlines' 322 | click_fn: gridlines_item_click 323 | uicon: '\uEA72' 324 | ), 325 | ] 326 | ) 327 | } 328 | 329 | fn gridlines_item_click(mut e ui.MouseEvent) { 330 | mut app := e.ctx.win.get[&App]('app') 331 | app.settings.show_gridlines = !app.settings.show_gridlines 332 | app.settings_save() or {} 333 | } 334 | 335 | fn size_menu_item(size int) &ui.MenuItem { 336 | item := ui.MenuItem.new( 337 | text: '${size} px' 338 | click_event_fn: menu_size_click 339 | ) 340 | return item 341 | } 342 | 343 | fn menu_size_custom_click(mut win ui.Window, com ui.MenuItem) { 344 | mut app := win.get[&App]('app') 345 | app.show_size_modal() 346 | } 347 | 348 | fn menu_resize_click(mut win ui.Window, com ui.MenuItem) { 349 | mut app := win.get[&App]('app') 350 | app.show_resize_modal(app.canvas.w, app.canvas.h) 351 | } 352 | 353 | fn menu_size_click(mut win ui.Window, com ui.MenuItem) { 354 | mut app := win.get[&App]('app') 355 | size := com.text.replace(' px', '').int() 356 | app.brush_size = size 357 | } 358 | 359 | fn menubar_fit_zoom_click(mut win ui.Window, com ui.MenuItem) { 360 | mut app := win.get[&App]('app') 361 | canvas_height := app.sv.height - 50 362 | level := canvas_height / app.data.file.height 363 | app.canvas.set_zoom(level) 364 | } 365 | 366 | // Change Window Theme 367 | fn theme_click(mut win ui.Window, com ui.MenuItem) { 368 | text := com.text 369 | mut theme := ui.theme_by_name(text) 370 | win.set_theme(theme) 371 | 372 | mut app := win.get[&App]('app') 373 | app.settings.theme = text 374 | app.set_theme_bg(text) 375 | app.settings_save() or {} 376 | } 377 | 378 | fn (mut app App) set_theme(name string) { 379 | mut theme := ui.theme_by_name(name) 380 | app.win.set_theme(theme) 381 | app.settings.theme = name 382 | app.set_theme_bg(name) 383 | app.settings_save() or {} 384 | } 385 | 386 | fn (mut app App) set_theme_bg(text string) { 387 | if text.contains('Dark') { 388 | background := gx.rgb(25, 42, 77) 389 | app.win.gg.set_bg_color(gx.rgb(25, 42, 77)) 390 | app.win.id_map['background'] = &background 391 | } else if text.contains('Black') { 392 | app.win.gg.set_bg_color(gx.rgb(0, 0, 0)) 393 | background := gx.rgb(0, 0, 0) 394 | app.win.id_map['background'] = &background 395 | } else if text.contains('Green Mono') { 396 | app.win.gg.set_bg_color(gx.rgb(0, 16, 0)) 397 | background := gx.rgb(0, 16, 0) 398 | app.win.id_map['background'] = &background 399 | } else { 400 | background := gx.rgb(210, 220, 240) 401 | app.win.gg.set_bg_color(background) 402 | app.win.id_map['background'] = &background 403 | } 404 | } 405 | 406 | fn settings_click(mut win ui.Window, com ui.MenuItem) { 407 | mut app := win.get[&App]('app') 408 | app.show_settings() 409 | } 410 | 411 | fn about_click(mut win ui.Window, com ui.MenuItem) { 412 | mut modal := ui.Modal.new(title: 'About vPaint') 413 | 414 | modal.top_off = 25 415 | modal.in_width = 300 416 | modal.in_height = 290 417 | 418 | mut title := ui.Label.new( 419 | text: 'VPaint' 420 | bold: true 421 | em_size: 2 422 | vertical_align: .middle 423 | pack: true 424 | ) 425 | 426 | mut p := ui.Panel.new( 427 | layout: ui.BorderLayout.new( 428 | hgap: 20 429 | ) 430 | ) 431 | p.add_child_with_flag(title, ui.borderlayout_north) 432 | 433 | mut lbl := ui.Label.new( 434 | text: about_text.join('\n') 435 | pack: true 436 | vertical_align: .middle 437 | ) 438 | p.add_child_with_flag(lbl, ui.borderlayout_center) 439 | 440 | mut lp := ui.Panel.new( 441 | layout: ui.BoxLayout.new( 442 | ori: 0 443 | hgap: 30 444 | ) 445 | ) 446 | lp.set_bounds(0, 0, modal.in_width - 32, 30) 447 | 448 | icons8 := ui.link( 449 | text: 'Icons8' 450 | url: 'https://icons8.com/' 451 | pack: true 452 | ) 453 | 454 | git := ui.link( 455 | text: 'Github' 456 | url: 'https://github.com/pisaiah/vpaint' 457 | pack: true 458 | ) 459 | 460 | vlang := ui.link( 461 | text: 'About V' 462 | url: 'https://vlang.io' 463 | pack: true 464 | ) 465 | 466 | p.set_bounds(0, 9, modal.in_width, modal.in_height - 100) 467 | lp.add_child(icons8) 468 | lp.add_child(git) 469 | lp.add_child(vlang) 470 | p.add_child_with_flag(lp, ui.borderlayout_south) 471 | 472 | modal.add_child(p) 473 | modal.make_close_btn(true) 474 | modal.close.set_bounds((modal.in_width / 2) - 50, modal.in_height - 45, 100, 30) 475 | modal.needs_init = false 476 | 477 | win.add_child(modal) 478 | } 479 | -------------------------------------------------------------------------------- /src/paint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | import gx 6 | 7 | // Version 8 | const version = '0.6-dev' 9 | 10 | // About Info 11 | const about_text = [ 12 | 'Simple Image Editor written in the V Language.', 13 | '(version ${version}) (iUI: ${ui.version})', 14 | '\t ', 15 | 'Copyright \u00A9 2022-2025 Isaiah.', 16 | 'Released under MIT License.', 17 | ] 18 | 19 | // Settings 20 | struct Settings { 21 | mut: 22 | autohide_sidebar bool 23 | theme string = 'Default' 24 | round_ends bool = true 25 | show_gridlines bool = true 26 | } 27 | 28 | // Our Paint App 29 | @[heap] 30 | struct App { 31 | mut: 32 | win &ui.Window 33 | sv &ui.ScrollView 34 | sidebar &ui.Panel 35 | canvas_zoom int 36 | data &ImageViewData 37 | canvas &Image 38 | color gx.Color 39 | color_2 gx.Color = gx.white 40 | sele_color bool 41 | tool &Tool 42 | ribbon &ui.Panel 43 | status_bar &ui.Panel 44 | stat_lbl &ui.Label 45 | brush_size int = 1 46 | bg_id int 47 | need_open bool 48 | settings &Settings 49 | wasm_load_tick int 50 | cp &ColorPicker 51 | } 52 | 53 | fn (app &App) get_color() gx.Color { 54 | if app.sele_color { 55 | return app.color_2 56 | } 57 | return app.color 58 | } 59 | 60 | fn (mut app App) set_color(c gx.Color) { 61 | if app.sele_color { 62 | app.color_2 = c 63 | return 64 | } 65 | app.color = c 66 | } 67 | 68 | fn main() { 69 | // Create Window 70 | mut window := ui.Window.new( 71 | title: 'vPaint ${version}' 72 | width: 550 // 700 73 | height: 450 74 | font_size: 16 75 | ui_mode: false 76 | ) 77 | 78 | mut app := &App{ 79 | sv: unsafe { nil } 80 | sidebar: ui.Panel.new(layout: ui.FlowLayout.new(vgap: 1, hgap: 1)) 81 | data: unsafe { nil } 82 | canvas: unsafe { nil } 83 | win: window 84 | ribbon: ui.Panel.new(layout: ui.BoxLayout.new(vgap: 4, hgap: 4)) 85 | status_bar: unsafe { nil } 86 | stat_lbl: unsafe { nil } 87 | tool: &PencilTool{} 88 | settings: &Settings{} 89 | cp: unsafe { nil } 90 | } 91 | window.id_map['app'] = app 92 | 93 | app.settings_load() or { println('Error loading settings: ${err}') } 94 | app.make_menubar(mut window) 95 | 96 | mut path := os.resource_abs_path('untitledv.png') 97 | 98 | if os.args.len > 1 { 99 | path = os.real_path(os.args[1]) 100 | } 101 | 102 | if !os.exists(path) { 103 | mut blank_png := $embed_file('blank.png') 104 | os.write_file_array(path, blank_png.to_bytes()) or { panic(error) } 105 | } 106 | 107 | mut image_panel := app.make_image_view(path) 108 | 109 | if '-upscale' in os.args { 110 | println('Upscaling ${os.args[1]}...') 111 | out_path := os.args[3].split('-path=')[1] 112 | app.canvas.scale2x() 113 | app.write_img(app.data.file, out_path) 114 | return 115 | } 116 | 117 | if os.args.len == 4 && os.args[2].contains('-upscale=') { 118 | times := os.args[2].split('-upscale=')[1].int() 119 | 120 | println('Upscaling ${times}x "${os.args[1]}"...') 121 | out_path := os.args[3].split('-path=')[1] 122 | 123 | for _ in 0 .. times { 124 | app.canvas.scale2x() 125 | } 126 | app.write_img(app.data.file, out_path) 127 | return 128 | } 129 | 130 | mut sv := &ui.ScrollView{ 131 | children: [image_panel] 132 | increment: 2 133 | padding: 50 134 | } 135 | app.sv = sv 136 | sv.set_bounds(0, 0, 500, 210) 137 | 138 | app.make_sidebar() 139 | 140 | // Ribbon bar 141 | // app.ribbon.z_index = 21 142 | app.ribbon.subscribe_event('draw', ribbon_draw_fn) 143 | 144 | app.make_ribbon() 145 | 146 | mut sb := app.make_status_bar(window) 147 | app.status_bar = sb 148 | 149 | mut pan := ui.Panel.new( 150 | layout: ui.BorderLayout.new( 151 | hgap: 0 152 | vgap: 0 153 | ) 154 | ) 155 | 156 | pan.add_child(app.ribbon, value: ui.borderlayout_north) 157 | pan.add_child_with_flag(app.sidebar, ui.borderlayout_west) 158 | pan.add_child_with_flag(app.sv, ui.borderlayout_center) 159 | pan.add_child_with_flag(sb, ui.borderlayout_south) 160 | 161 | window.add_child(pan) 162 | 163 | mut win := app.win 164 | tb_file := $embed_file('assets/checker2.png') 165 | data := tb_file.to_bytes() 166 | gg_im := win.gg.create_image_from_byte_array(data) or { panic(err) } 167 | cim := win.gg.cache_image(gg_im) 168 | app.bg_id = cim 169 | 170 | background := gx.rgb(210, 220, 240) 171 | window.gg.set_bg_color(background) 172 | 173 | app.set_theme_bg(app.win.theme.name) 174 | 175 | window.gg.run() 176 | } 177 | -------------------------------------------------------------------------------- /src/resize_modal.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | // Resize Modal 7 | fn (mut app App) show_resize_modal(cw int, ch int) { 8 | mut modal := ui.Modal.new( 9 | title: 'Resize Canvas' 10 | width: 300 11 | height: 200 12 | ) 13 | 14 | mut width_box := ui.text_field(text: '${cw}') 15 | mut heigh_box := ui.text_field(text: '${ch}') 16 | 17 | mut width_lbl := ui.Label.new(text: 'Width') 18 | mut heigh_lbl := ui.Label.new(text: 'Height') 19 | 20 | mut p := ui.Panel.new( 21 | layout: ui.GridLayout.new(rows: 2) 22 | ) 23 | p.set_bounds(25, 25, 250, 80) 24 | 25 | p.add_child(width_lbl) 26 | p.add_child(heigh_lbl) 27 | p.add_child(width_box) 28 | p.add_child(heigh_box) 29 | modal.add_child(p) 30 | 31 | width_box.set_id(mut app.win, 'resize_width') 32 | heigh_box.set_id(mut app.win, 'resize_heigh') 33 | 34 | modal.needs_init = false 35 | create_close_btn(mut modal, app.win) 36 | 37 | app.win.add_child(modal) 38 | } 39 | 40 | pub fn create_close_btn(mut this ui.Modal, app &ui.Window) &ui.Button { 41 | mut close := ui.Button.new(text: 'OK') 42 | mut cancel := ui.Button.new(text: 'Cancel') 43 | 44 | y := this.in_height - 45 45 | close.set_accent_filled(true) 46 | close.set_bounds(24, y, 130, 30) 47 | cancel.set_bounds(165, y, 105, 30) 48 | 49 | close.subscribe_event('mouse_up', resize_close_click) 50 | cancel.subscribe_event('mouse_up', end_modal) 51 | 52 | this.add_child(cancel) 53 | 54 | this.children << close 55 | this.close = close 56 | return close 57 | } 58 | 59 | fn resize_close_click(mut e ui.MouseEvent) { 60 | e.ctx.win.components = e.ctx.win.components.filter(mut it !is ui.Modal) 61 | mut width_lbl := e.ctx.win.get[&ui.TextField]('resize_width') 62 | mut heigh_lbl := e.ctx.win.get[&ui.TextField]('resize_heigh') 63 | mut app := e.ctx.win.get[&App]('app') 64 | 65 | app.canvas.resize(width_lbl.text.int(), heigh_lbl.text.int()) 66 | } 67 | 68 | // Custom Pencil Button 69 | fn (mut app App) show_custom_pencil_modal() { 70 | mut modal := ui.Modal.new(title: 'Custom Pencil Tool') 71 | 72 | modal.in_width = 300 73 | modal.in_height = 200 74 | 75 | mut tool := app.tool 76 | 77 | mut width_box := ui.text_field(text: '0') 78 | mut heigh_box := ui.text_field(text: '0') 79 | 80 | if mut tool is CustomPencilTool { 81 | width_box.text = '${tool.width}' 82 | heigh_box.text = '${tool.height}' 83 | } 84 | 85 | mut width_lbl := ui.Label.new(text: 'Width') 86 | mut heigh_lbl := ui.Label.new(text: 'Height') 87 | 88 | mut p := ui.Panel.new( 89 | layout: ui.GridLayout.new(rows: 3) 90 | ) 91 | p.set_bounds(25, 25, 250, 80) 92 | 93 | mut info := ui.Label.new(text: 'Override Size (0 = No Override, use tool size)') 94 | p.add_child(info) 95 | 96 | mut lbl2 := ui.Label.new() 97 | p.add_child(lbl2) 98 | 99 | p.add_child(width_lbl) 100 | p.add_child(heigh_lbl) 101 | p.add_child(width_box) 102 | p.add_child(heigh_box) 103 | modal.add_child(p) 104 | 105 | width_box.set_id(mut app.win, 'over_width') 106 | heigh_box.set_id(mut app.win, 'over_heigh') 107 | 108 | modal.needs_init = false 109 | create_close_btn_2(mut modal, app.win) 110 | 111 | app.win.add_child(modal) 112 | } 113 | 114 | pub fn create_close_btn_2(mut this ui.Modal, app &ui.Window) &ui.Button { 115 | mut close := ui.Button.new(text: 'OK') 116 | mut cancel := ui.Button.new(text: 'Cancel') 117 | 118 | y := this.in_height - 45 119 | close.set_bounds(24, y, 130, 30) 120 | cancel.set_bounds(165, y, 105, 30) 121 | 122 | close.subscribe_event('mouse_up', customp_close_click) 123 | cancel.subscribe_event('mouse_up', end_modal) 124 | 125 | this.add_child(cancel) 126 | 127 | this.children << close 128 | this.close = close 129 | return close 130 | } 131 | 132 | fn customp_close_click(mut e ui.MouseEvent) { 133 | e.ctx.win.components = e.ctx.win.components.filter(mut it !is ui.Modal) 134 | mut width_lbl := e.ctx.win.get[&ui.TextField]('over_width') 135 | mut heigh_lbl := e.ctx.win.get[&ui.TextField]('over_heigh') 136 | 137 | mut app := e.ctx.win.get[&App]('app') 138 | mut tool := app.tool 139 | 140 | if mut tool is CustomPencilTool { 141 | tool.width = width_lbl.text.int() 142 | tool.height = heigh_lbl.text.int() 143 | } 144 | } 145 | 146 | fn (mut app App) show_size_modal() { 147 | mut modal := ui.Modal.new( 148 | title: 'Set Brush Size' 149 | width: 245 150 | height: 210 151 | ) 152 | 153 | mut width_box := ui.numeric_field(app.brush_size) 154 | 155 | mut width_lbl := ui.Label.new( 156 | text: 'Tool/Brush Size (px)' 157 | pack: true 158 | ) 159 | 160 | width_lbl.set_pos(60, 34) 161 | width_box.set_bounds(20, 64, 200, 40) 162 | 163 | modal.add_child(width_lbl) 164 | modal.add_child(width_box) 165 | 166 | width_box.set_id(mut app.win, 'bs_size') 167 | 168 | modal.needs_init = false 169 | bs_create_close_btn(mut modal) 170 | 171 | app.win.add_child(modal) 172 | app.canvas.is_mouse_down = false 173 | } 174 | 175 | pub fn bs_create_close_btn(mut this ui.Modal) &ui.Button { 176 | y := this.in_height - 50 177 | 178 | mut close := ui.Button.new( 179 | text: 'OK' 180 | bounds: ui.Bounds{12, y, 120, 35} 181 | ) 182 | 183 | mut cancel := ui.Button.new( 184 | text: 'Cancel' 185 | bounds: ui.Bounds{138, y, 90, 35} 186 | ) 187 | 188 | close.set_accent_filled(true) 189 | close.subscribe_event('mouse_up', close_modal) 190 | cancel.subscribe_event('mouse_up', end_modal) 191 | 192 | this.add_child(cancel) 193 | 194 | this.children << close 195 | this.close = close 196 | return close 197 | } 198 | 199 | fn close_modal(mut e ui.MouseEvent) { 200 | mut win := e.ctx.win 201 | win.components = win.components.filter(mut it !is ui.Modal) 202 | mut width_lbl := win.get[&ui.TextField]('bs_size') 203 | mut app := win.get[&App]('app') 204 | 205 | app.brush_size = width_lbl.text.int() 206 | } 207 | 208 | fn end_modal(mut e ui.MouseEvent) { 209 | mut win := e.ctx.win 210 | win.components = win.components.filter(mut it !is ui.Modal) 211 | } 212 | 213 | // Prop Modal 214 | fn (mut app App) show_prop_modal(g &ui.GraphicsContext) { 215 | mut modal := ui.Modal.new( 216 | title: 'Image Properties' 217 | width: 250 218 | height: 250 219 | ) 220 | 221 | txt := [ 222 | 'Image Size:: ${app.canvas.w} x ${app.canvas.h} px', 223 | 'Zoom:: ${app.canvas.zoom}x', 224 | 'Megapixel:: ${f32(app.canvas.w * app.canvas.h) / 1000000} MP', 225 | ] 226 | 227 | txt2 := [ 228 | 'File Size:: ${app.data.file_size}', 229 | 'File Name:: ${os.base(app.data.file_name)}', 230 | ] 231 | 232 | mut gp := ui.Panel.new( 233 | layout: ui.GridLayout.new(cols: 2) 234 | ) 235 | 236 | for line in txt { 237 | for spl in line.split(': ') { 238 | mut lbl := ui.Label.new( 239 | text: spl 240 | pack: true 241 | ) 242 | gp.add_child(lbl) 243 | } 244 | } 245 | 246 | mut fp := ui.Panel.new( 247 | layout: ui.GridLayout.new(cols: 2) 248 | ) 249 | 250 | for line in txt2 { 251 | for spl in line.split(': ') { 252 | mut lbl := ui.Label.new( 253 | text: spl 254 | pack: true 255 | ) 256 | fp.add_child(lbl) 257 | } 258 | 259 | w := g.text_width(line) + 40 260 | if modal.in_width < w { 261 | modal.in_width = w 262 | } 263 | } 264 | 265 | mut p := ui.Panel.new( 266 | layout: ui.BoxLayout.new(ori: 1) 267 | ) 268 | 269 | p.set_pos(5, 5) 270 | p.add_child(gp) 271 | p.add_child(fp) 272 | 273 | modal.add_child(p) 274 | 275 | app.win.add_child(modal) 276 | app.canvas.is_mouse_down = false 277 | } 278 | 279 | @[heap] 280 | struct NewModal { 281 | ui.Modal 282 | mut: 283 | // modal &ui.Modal 284 | w_box &ui.TextField 285 | h_box &ui.TextField 286 | same bool 287 | } 288 | 289 | // Resize Modal 290 | fn (mut app App) show_new_modal(cw int, ch int) { 291 | mut width_box := ui.TextField.new(text: '${cw}') 292 | mut heigh_box := ui.TextField.new(text: '${ch}') 293 | 294 | mut width_lbl := ui.Label.new(text: 'Width') 295 | mut blank_lbl := ui.Label.new(text: ' ') 296 | mut heigh_lbl := ui.Label.new(text: 'Height') 297 | 298 | mut nm := &NewModal{ 299 | text: 'New!' 300 | in_width: 300 301 | in_height: 250 302 | z_index: 500 303 | close: unsafe { nil } 304 | w_box: width_box 305 | h_box: heigh_box 306 | same: true 307 | } 308 | 309 | width_box.subscribe_event('text_change', nm.text_change_fn) 310 | heigh_box.subscribe_event('text_change', nm.text_change_fn) 311 | 312 | mut link := ui.Button.new( 313 | text: '\ue167' 314 | pack: true 315 | ) 316 | link.set_accent_filled(true) 317 | link.font = 1 318 | link.subscribe_event('mouse_up', nm.button_click_fn) 319 | 320 | mut p := ui.Panel.new( 321 | layout: ui.GridLayout.new(cols: 3) 322 | ) 323 | p.set_bounds(20, 20, 260, 90) 324 | 325 | p.add_child(width_lbl) 326 | p.add_child(blank_lbl) 327 | p.add_child(heigh_lbl) 328 | p.add_child(width_box) 329 | p.add_child(link) 330 | p.add_child(heigh_box) 331 | 332 | nm.add_child(p) 333 | 334 | nm.needs_init = false 335 | nm.new_modal_close_btn() 336 | 337 | nm.set_bounds(0, 0, 1280, 720) 338 | 339 | app.win.add_child(nm) 340 | } 341 | 342 | pub fn (mut nm NewModal) button_click_fn(mut e ui.MouseEvent) { 343 | nm.same = !nm.same 344 | 345 | mut tar := e.target 346 | if mut tar is ui.Button { 347 | tar.set_accent_filled(nm.same) 348 | } 349 | } 350 | 351 | pub fn (mut nm NewModal) text_change_fn(mut e ui.TextChangeEvent) { 352 | if nm.same { 353 | nm.w_box.text = e.target.text 354 | nm.h_box.text = e.target.text 355 | nm.w_box.carrot_left = e.target.text.len 356 | nm.h_box.carrot_left = e.target.text.len 357 | } 358 | } 359 | 360 | pub fn (mut nm NewModal) new_modal_close_btn() &ui.Button { 361 | mut close := ui.Button.new(text: 'OK') 362 | mut cancel := ui.Button.new(text: 'Cancel') 363 | 364 | y := nm.in_height - 45 365 | close.set_accent_filled(true) 366 | close.set_bounds(20, y, 150, 30) 367 | cancel.set_bounds(175, y, 105, 30) 368 | 369 | close.subscribe_event('mouse_up', nm.new_close_click) 370 | cancel.subscribe_event('mouse_up', end_modal2) 371 | 372 | nm.add_child(cancel) 373 | 374 | nm.children << close 375 | nm.close = close 376 | return close 377 | } 378 | 379 | fn end_modal2(mut e ui.MouseEvent) { 380 | mut win := e.ctx.win 381 | win.components = win.components.filter(mut it !is NewModal) 382 | } 383 | 384 | fn (mut nm NewModal) new_close_click(mut e ui.MouseEvent) { 385 | // TODO: prompt 'do you want to save' 386 | 387 | mut app := e.ctx.win.get[&App]('app') 388 | app.load_new(nm.w_box.text.int(), nm.h_box.text.int()) 389 | 390 | e.ctx.win.components = e.ctx.win.components.filter(mut it !is NewModal) 391 | } 392 | -------------------------------------------------------------------------------- /src/ribbon.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | // import gg 4 | import gx 5 | import iui as ui 6 | 7 | fn (mut app App) make_ribbon() { 8 | mut box1 := ui.Panel.new(layout: ui.BoxLayout.new(ori: 1, hgap: 0)) 9 | 10 | mut color_box := app.make_color_box() 11 | 12 | box1.add_child(make_c_btn(0)) 13 | box1.add_child(make_c_btn(10)) 14 | 15 | // Eye Dropper 16 | img_picker_file := $embed_file('assets/rgb-picker.png') 17 | mut btn := app.ribbon_icon_btn(img_picker_file.to_bytes()) 18 | 19 | app.ribbon.height = 74 20 | 21 | box1.set_x(5) 22 | color_box.set_x(11) 23 | btn.set_x(5) 24 | btn.border_radius = 2 25 | 26 | btn.y = 0 27 | btn.height = app.ribbon.height - 10 28 | 29 | app.ribbon.add_child(box1) 30 | app.ribbon.add_child(color_box) 31 | app.ribbon.add_child(btn) 32 | 33 | img_file := $embed_file('assets/hsv.png') 34 | data := img_file.to_bytes() 35 | 36 | mut gg := app.win.gg 37 | gg_im := gg.create_image_from_byte_array(data) or { panic(err) } 38 | 39 | mut cim := 0 40 | cim = gg.cache_image(gg_im) 41 | app.win.id_map['HSL'] = &cim 42 | 43 | mut sp := ui.Panel.new(layout: ui.FlowLayout.new(hgap: 1, vgap: 1)) 44 | sp.subscribe_event('draw', app.shape_box_draw) 45 | sp.set_bounds(5, 0, 50, app.ribbon.height - 10) 46 | 47 | for i, label in shape_labels { 48 | mut sbtn := ui.Button.new( 49 | text: shape_uicons[i] 50 | ) 51 | sbtn.extra = label 52 | sbtn.subscribe_event('mouse_up', app.shape_btn_click) 53 | sbtn.set_bounds(0, 0, 23, 20) 54 | sbtn.font_size = 12 55 | sbtn.font = 1 56 | sbtn.border_radius = -1 57 | sbtn.set_area_filled_state(false, .normal) 58 | sp.add_child(sbtn) 59 | } 60 | 61 | app.ribbon.add_child(sp) 62 | } 63 | 64 | fn (mut app App) shape_btn_click(e &ui.MouseEvent) { 65 | mut tar := e.target 66 | if mut tar is ui.Button { 67 | app.set_tool_by_name(tar.extra) 68 | } 69 | } 70 | 71 | fn draw_box_border(com &ui.Component, g &ui.GraphicsContext, mw int) { 72 | g.draw_rounded_rect(com.x, com.y, com.width - mw, com.height, 4, g.theme.button_border_normal, 73 | g.theme.textbox_background) 74 | } 75 | 76 | fn (mut app App) make_color_box() &ui.Panel { 77 | mut color_box := ui.Panel.new( 78 | layout: ui.GridLayout.new(rows: 2, vgap: 3, hgap: 3) 79 | ) 80 | 81 | colors := [gx.rgb(0, 0, 0), gx.rgb(127, 127, 127), gx.rgb(136, 0, 21), 82 | gx.rgb(237, 28, 36), gx.rgb(255, 127, 39), gx.rgb(255, 242, 0), 83 | gx.rgb(34, 177, 76), gx.rgb(0, 162, 232), gx.rgb(63, 72, 204), 84 | gx.rgb(163, 73, 164), gx.rgb(255, 255, 255), gx.rgb(195, 195, 195), 85 | gx.rgb(185, 122, 87), gx.rgb(255, 174, 201), gx.rgb(255, 200, 15), 86 | gx.rgb(239, 228, 176), gx.rgb(180, 230, 30), gx.rgb(153, 217, 235), 87 | gx.rgb(112, 146, 190), gx.rgba(0, 0, 0, 0)] 88 | 89 | // gx.rgba(200, 190, 230, 0) 90 | 91 | size := 24 92 | 93 | for color in colors { 94 | mut btn := ui.Button.new(text: ' ') 95 | btn.set_background(color) 96 | btn.border_radius = 32 97 | btn.subscribe_event('mouse_up', fn [mut app, color] (mut e ui.MouseEvent) { 98 | mut btn := e.target 99 | if mut btn is ui.Button { 100 | btn_color := btn.override_bg_color 101 | if btn_color != color { 102 | // WASM does not support Closures 103 | // dump('Debug: Problem with Wasm closure') 104 | app.set_color(btn_color) 105 | return 106 | } 107 | } 108 | 109 | app.set_color(color) 110 | }) 111 | color_box.add_child(btn) 112 | } 113 | 114 | color_box.subscribe_event('draw', fn [mut color_box] (mut e ui.DrawEvent) { 115 | w := e.target.parent.width 116 | if w < 385 { 117 | aa := w - 95 118 | color_box.width = aa 119 | } else if color_box.width < 300 { 120 | color_box.width = (24 + 6) * 10 121 | } 122 | 123 | draw_box_border(e.target, e.ctx, 6) 124 | }) 125 | 126 | color_box.set_background(gx.rgba(0, 0, 0, 1)) 127 | color_box.set_bounds(0, 0, (size + 6) * 10, 64) 128 | return color_box 129 | } 130 | 131 | fn make_c_btn(count int) &ui.Button { 132 | txt := if count == 0 { '' } else { ' ' } 133 | mut current_btn := ui.Button.new(text: txt) 134 | current_btn.set_bounds(0, 0, 26, 26) 135 | current_btn.border_radius = 16 136 | current_btn.subscribe_event('draw', current_color_btn_draw) 137 | return current_btn 138 | } 139 | 140 | fn (mut app App) ribbon_icon_btn(data []u8) &ui.Button { 141 | mut gg := app.win.gg 142 | gg_im := gg.create_image_from_byte_array(data) or { panic(err) } 143 | cim := gg.cache_image(gg_im) 144 | mut btn := ui.Button.new(icon: cim) 145 | btn.set_bounds(0, 14, 32, 36) 146 | btn.icon_width = 32 147 | btn.icon_height = 32 148 | 149 | btn.subscribe_event('mouse_up', fn [mut app] (mut e ui.MouseEvent) { 150 | mut win := e.ctx.win 151 | 152 | if isnil(app.cp) { 153 | app.cp = ColorPicker.new() 154 | 155 | app.cp.subscribe_event('color_picked', fn [mut app] (cp &ColorPicker) { 156 | app.set_color(cp.color) 157 | }) 158 | } 159 | 160 | mut modal := app.cp.open_color_picker(app.get_color()) 161 | win.add_child(modal) 162 | }) 163 | return btn 164 | } 165 | 166 | fn current_color_btn_draw(mut e ui.DrawEvent) { 167 | mut com := e.target 168 | mut win := e.ctx.win 169 | if mut com is ui.Button { 170 | mut app := win.get[&App]('app') 171 | bg := if com.text == '' { app.color } else { app.color_2 } 172 | com.set_background(bg) 173 | sele := (com.text == ' ' && app.sele_color) || (com.text == '' && !app.sele_color) 174 | if sele { 175 | o := 3 176 | ry := com.ry 177 | width := com.width + (o * 2) 178 | heigh := com.height + (o * 2) 179 | e.ctx.gg.draw_rounded_rect_filled(com.rx - o, ry - o, width, heigh, 16, e.ctx.theme.button_bg_hover) 180 | e.ctx.gg.draw_rounded_rect_empty(com.rx - o, ry - o, width, heigh, 16, e.ctx.theme.accent_fill) 181 | } else if com.is_mouse_rele { 182 | app.sele_color = !app.sele_color 183 | } 184 | } 185 | } 186 | 187 | // fn ribbon_draw_fn(mut win ui.Window, mut com ui.Component) { 188 | fn ribbon_draw_fn(mut e ui.DrawEvent) { 189 | color := e.ctx.theme.textbox_background 190 | e.ctx.gg.draw_rect_filled(e.target.x, e.target.y - 1, e.target.width, e.target.height + 1, 191 | color) 192 | 193 | hid := e.target.width < 400 194 | if e.target.children.len < 3 { 195 | return 196 | } 197 | mut child := e.target.children[3] 198 | if mut child is ui.Panel { 199 | child.hidden = hid 200 | } 201 | } 202 | 203 | fn (mut app App) shape_box_draw(mut e ui.DrawEvent) { 204 | draw_box_border(e.target, e.ctx, 0) 205 | } 206 | -------------------------------------------------------------------------------- /src/selection.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gx 5 | import math 6 | import gg 7 | 8 | // Select Tool 9 | // TODO: Implement Selection 10 | struct SelectTool { 11 | tool_name string = 'Select' 12 | mut: 13 | dx int = -1 14 | dy int 15 | selection Selection = Selection{-1, -1, -1, -1, [][]gx.Color{}, ui.Bounds{}} 16 | sx f32 = -1 17 | sy f32 18 | moving bool 19 | } 20 | 21 | struct Selection { 22 | mut: 23 | x1 int 24 | y1 int 25 | x2 int 26 | y2 int 27 | px [][]gx.Color 28 | og ui.Bounds 29 | } 30 | 31 | pub fn (mut sel Selection) clear_before(a voidptr) { 32 | mut img := unsafe { &Image(a) } 33 | for x in 0 .. (sel.x2 - sel.x1) { 34 | for y in 0 .. (sel.y2 - sel.y1) { 35 | img.set2(sel.og.x + x, sel.og.y + y, img.app.color_2, true) 36 | } 37 | } 38 | img.refresh() 39 | } 40 | 41 | pub fn (mut sel Selection) fill_px(a voidptr) { 42 | mut img := unsafe { &Image(a) } 43 | 44 | for x in sel.x1 .. sel.x2 + 1 { 45 | for y in sel.y1 .. sel.y2 + 1 { 46 | sel.px[x - sel.x1][y - sel.y1] = img.get(x, y) 47 | } 48 | } 49 | } 50 | 51 | pub fn (sel Selection) is_in(img &Image, px f32, py f32) bool { 52 | x1, y1 := img.get_point_screen_pos(sel.x1 - 1, sel.y1 - 1) 53 | x2, y2 := img.get_point_screen_pos(sel.x2, sel.y2) 54 | 55 | width := (x2 - x1) + img.zoom 56 | height := (y2 - y1) + img.zoom 57 | 58 | x := x1 59 | y := y1 60 | 61 | midx := x + (width / 2) 62 | midy := y + (height / 2) 63 | 64 | return math.abs(midx - px) < (width / 2) && math.abs(midy - py) < (height / 2) 65 | } 66 | 67 | fn pos_only(num int) int { 68 | return if num < 0 { 0 } else { num } 69 | } 70 | 71 | fn (mut this SelectTool) draw_moving_drag(img &Image, ctx &ui.GraphicsContext) { 72 | swidth := this.selection.x2 - this.selection.x1 73 | sheight := this.selection.y2 - this.selection.y1 74 | 75 | tsx, tsy := img.get_pos_point(this.sx, this.sy) 76 | dsx := tsx - this.selection.x1 77 | dsy := tsy - this.selection.y1 78 | 79 | mx_ := img.mx - dsx 80 | my_ := img.my - dsy 81 | 82 | mx := if mx_ + swidth >= img.w { img.w - swidth - 1 } else { pos_only(mx_) } 83 | my := if my_ + sheight >= img.h { img.h - sheight - 1 } else { pos_only(my_) } 84 | 85 | this.selection.x1 = mx 86 | this.selection.y1 = my 87 | 88 | this.selection.x2 = this.selection.x1 + swidth 89 | this.selection.y2 = this.selection.y1 + sheight 90 | 91 | this.sx, this.sy = img.get_point_screen_pos(mx + dsx, my + dsy) 92 | } 93 | 94 | fn (mut this SelectTool) draw_moving(img &Image, ctx &ui.GraphicsContext) { 95 | this.moving = true 96 | 97 | x1, y1 := img.get_point_screen_pos(this.selection.x1, this.selection.y1) 98 | x2, y2 := img.get_point_screen_pos(this.selection.x2, this.selection.y2) 99 | x3, y3 := img.get_point_screen_pos(this.selection.og.x, this.selection.og.y) 100 | 101 | sw := this.selection.x2 - this.selection.x1 + 1 102 | sh := this.selection.y2 - this.selection.y1 + 1 103 | 104 | // Fake removing old pixels 105 | /* 106 | ctx.gg.draw_image_with_config(gg.DrawImageConfig{ 107 | img_id: img.app.bg_id 108 | img_rect: gg.Rect{ 109 | x: x3 110 | y: y3 111 | width: sw * img.zoom 112 | height: sh * img.zoom 113 | } 114 | }) 115 | ctx.gg.draw_rounded_rect_filled(x3, y3, sw * img.zoom, sh * img.zoom, 1, img.app.color_2) 116 | */ 117 | 118 | width := (x2 - x1) + img.zoom 119 | height := (y2 - y1) + img.zoom 120 | 121 | // Draw old rect 122 | if !(x1 == x3 && y1 == y3) { 123 | ctx.gg.draw_rounded_rect_empty(x3, y3, width, height, 1, gx.red) 124 | ctx.gg.draw_rounded_rect_filled(x3, y3, width, height, 1, gx.rgba(255, 0, 0, 80)) 125 | ctx.gg.draw_line(x3, y3, x3 + width, y3 + height, gx.red) 126 | ctx.gg.draw_line(x3, y3 + height, x3 + width, y3, gx.red) 127 | } 128 | 129 | // Draw new rect 130 | ctx.gg.draw_image_with_config(gg.DrawImageConfig{ 131 | img_id: img.img 132 | img_rect: gg.Rect{ 133 | x: x1 134 | y: y1 135 | width: x2 - x1 + img.zoom 136 | height: y2 - y1 + img.zoom 137 | } 138 | part_rect: gg.Rect{ 139 | x: this.selection.og.x 140 | y: this.selection.og.y 141 | width: sw 142 | height: sh 143 | } 144 | }) 145 | 146 | ctx.gg.draw_rounded_rect_empty(x1, y1, width, height, 1, gx.green) 147 | ctx.gg.draw_rounded_rect_filled(x1, y1, width, height, 1, gx.rgba(0, 255, 0, 50)) 148 | 149 | sx, sy := img.get_point_screen_pos(img.mx, img.my) 150 | 151 | if this.selection.is_in(img, sx, sy) && this.dx != -1 { 152 | this.draw_moving_drag(img, ctx) 153 | } 154 | } 155 | 156 | fn (mut this SelectTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 157 | mut img := unsafe { &Image(a) } 158 | 159 | if this.selection.x1 != -1 { 160 | this.draw_moving(img, ctx) 161 | return 162 | } 163 | 164 | if this.dx == -1 { 165 | return 166 | } 167 | 168 | xoff := img.mx - this.dx 169 | yoff := img.my - this.dy 170 | 171 | sx, sy := img.get_point_screen_pos(this.dx, this.dy) 172 | 173 | x := math.min(sx, sx + (img.zoom * xoff)) 174 | y := math.min(sy, sy + (img.zoom * yoff)) 175 | width := math.abs(img.zoom * xoff) + img.zoom 176 | height := math.abs(img.zoom * yoff) + img.zoom 177 | 178 | ctx.gg.draw_rounded_rect_empty(x, y, width, height, 1, ctx.theme.accent_fill) 179 | ctx.gg.draw_rounded_rect_filled(x, y, width, height, 1, gx.rgba(0, 0, 255, 50)) 180 | } 181 | 182 | fn (mut this SelectTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 183 | mut img := unsafe { &Image(a) } 184 | 185 | if this.dx == -1 { 186 | this.dx = img.mx 187 | this.dy = img.my 188 | } 189 | 190 | if this.sx == -1 { 191 | this.sx, this.sy = img.get_point_screen_pos(this.dx, this.dy) 192 | } 193 | } 194 | 195 | fn (mut this SelectTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 196 | mut img := unsafe { &Image(a) } 197 | 198 | if !this.moving { 199 | // Making Selection 200 | x1 := math.min(img.mx, this.dx) 201 | y1 := math.min(img.my, this.dy) 202 | x2 := math.max(img.mx, this.dx) 203 | y2 := math.max(img.my, this.dy) 204 | 205 | this.selection = Selection{ 206 | x1: x1 207 | y1: y1 208 | x2: x2 209 | y2: y2 210 | px: [][]gx.Color{len: (x2 - x1) + 1, init: []gx.Color{len: (y2 - y1) + 1}} 211 | og: ui.Bounds{x1, y1, x2, y2} 212 | } 213 | this.selection.fill_px(img) 214 | } else { 215 | // Clicked out of current Selection 216 | this.moving = false 217 | 218 | sx, sy := img.get_point_screen_pos(img.mx, img.my) 219 | if !this.selection.is_in(img, sx, sy) { 220 | sw := this.selection.x2 - this.selection.x1 + 1 221 | sh := this.selection.y2 - this.selection.y1 + 1 222 | 223 | // img.note_multichange() 224 | 225 | mut change := Multichange.new() 226 | 227 | for x in 0 .. sw { 228 | for y in 0 .. sh { 229 | img.set_raw(this.selection.og.x + x, this.selection.og.y + y, img.app.color_2, mut 230 | change) 231 | } 232 | } 233 | 234 | img.push(change) 235 | change = Multichange.new() 236 | change.batch = true 237 | 238 | for x in 0 .. sw { 239 | for y in 0 .. sh { 240 | c := this.selection.px[x][y] 241 | 242 | // Set New 243 | img.set_raw(this.selection.x1 + x, this.selection.y1 + y, c, mut change) 244 | } 245 | } 246 | img.push(change) 247 | img.refresh() 248 | 249 | this.selection = Selection{-1, -1, -1, -1, [][]gx.Color{}, ui.Bounds{}} 250 | } 251 | } 252 | 253 | this.sx = -1 254 | this.dx = -1 255 | this.dy = -1 256 | } 257 | -------------------------------------------------------------------------------- /src/settings.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | fn (mut app App) show_settings() { 7 | mut page := ui.Page.new(title: 'Settings') 8 | 9 | mut p := ui.Panel.new( 10 | layout: ui.FlowLayout.new() 11 | ) 12 | 13 | mut panel := ui.Panel.new(layout: ui.BoxLayout.new(ori: 1)) 14 | 15 | // Auto-hide Sidebar 16 | mut card := ui.SettingsCard.new( 17 | uicon: '\uE700' 18 | text: 'Sidebar Hidden' 19 | description: 'Choose to autohide the side toolbar.' 20 | stretch: true 21 | ) 22 | mut box := ui.Checkbox.new(text: 'Autohide') 23 | box.is_selected = app.settings.autohide_sidebar 24 | box.set_bounds(0, 0, 100, 24) 25 | box.subscribe_event('mouse_up', app.hide_sidebar_mouse_up) 26 | 27 | card.add_child(box) 28 | 29 | // App Theme 30 | mut theme_card := ui.SettingsCard.new( 31 | uicon: '\uE790' 32 | text: 'App Theme' 33 | description: 'Choose how the app looks' 34 | stretch: true 35 | ) 36 | 37 | mut cb := ui.Selectbox.new( 38 | text: app.win.theme.name 39 | items: ui.get_all_themes().map(it.name) 40 | ) 41 | 42 | cb.set_bounds(0, 0, 120, 30) 43 | cb.subscribe_event('item_change', fn (mut e ui.ItemChangeEvent) { 44 | txt := e.target.text.replace('Light', 'Default') 45 | mut app := e.ctx.win.get[&App]('app') 46 | app.set_theme(txt) 47 | }) 48 | theme_card.add_child(cb) 49 | 50 | // Round card 51 | mut round_card := ui.SettingsCard.new( 52 | uicon: '\uF127' 53 | text: 'Round End Points' 54 | description: 'Round end-points of drawn lines' 55 | stretch: true 56 | ) 57 | 58 | mut rbox := ui.Switch.new(text: 'Round') 59 | rbox.is_selected = app.settings.round_ends 60 | rbox.set_bounds(0, 0, 100, 24) 61 | rbox.subscribe_event('mouse_up', app.round_ends_mouse_up) 62 | round_card.add_child(rbox) 63 | 64 | // Gridlines 65 | mut grid_card := ui.SettingsCard.new( 66 | uicon: '\uEA72' 67 | text: 'Show Gridlines' 68 | description: 'Choose to display gridlines on the Canvas.' 69 | stretch: true 70 | ) 71 | mut box2 := ui.Checkbox.new(text: 'Gridlines') 72 | box2.is_selected = app.settings.show_gridlines 73 | box2.set_bounds(0, 0, 100, 24) 74 | box2.subscribe_event('mouse_up', gridlines_item_click) 75 | 76 | grid_card.add_child(box2) 77 | 78 | panel.add_child(card) 79 | panel.add_child(theme_card) 80 | panel.add_child(round_card) 81 | panel.add_child(grid_card) 82 | 83 | // About Text 84 | 85 | mut lbl := ui.Label.new( 86 | text: 'About vPaint\n${about_text.join('\n')}\n' 87 | ) 88 | lbl.pack() 89 | 90 | mut about_p := ui.Panel.new(layout: ui.FlowLayout.new(hgap: 10, vgap: 10)) 91 | about_p.add_child(lbl) 92 | 93 | p.add_child(panel) 94 | p.add_child(about_p) 95 | 96 | p.subscribe_event('draw', fn (mut e ui.DrawEvent) { 97 | pw := e.ctx.gg.window_size().width 98 | tt := int(pw * f32(0.65)) 99 | size := if pw < 800 { pw } else { tt } 100 | e.target.children[0].width = size - 10 101 | }) 102 | 103 | page.add_child(p) 104 | app.win.add_child(page) 105 | } 106 | 107 | fn (mut app App) round_ends_mouse_up(mut e ui.MouseEvent) { 108 | // TODO 109 | app.settings.round_ends = !e.target.is_selected 110 | app.settings_save() or {} 111 | } 112 | 113 | fn (mut app App) hide_sidebar_mouse_up(mut e ui.MouseEvent) { 114 | // TODO 115 | app.settings.autohide_sidebar = !e.target.is_selected 116 | app.settings_save() or {} 117 | } 118 | 119 | const default_config = ['# VPaint Configuration File', 'theme: Default'] 120 | 121 | fn wasm_save_files() { 122 | $if emscripten ? { 123 | C.emscripten_run_script(c'iui.trigger = "savefiles"') 124 | } 125 | } 126 | 127 | fn wasm_load_files() { 128 | $if emscripten ? { 129 | C.emscripten_run_script(c'iui.trigger = "lloadfiles"') 130 | } 131 | } 132 | 133 | fn get_cfg_dir() string { 134 | $if emscripten ? { 135 | return os.home_dir() 136 | } 137 | return os.config_dir() or { os.home_dir() } 138 | } 139 | 140 | fn (mut app App) settings_load() ! { 141 | wasm_load_files() 142 | 143 | cfg_dir := get_cfg_dir() 144 | dir := os.join_path(cfg_dir, '.vpaint') 145 | file := os.join_path(dir, 'config.txt') 146 | 147 | if !os.exists(dir) { 148 | os.mkdir(dir) or { return err } 149 | } 150 | 151 | if !os.exists(file) { 152 | app.settings_save()! 153 | } 154 | 155 | lines := os.read_lines(file) or { return err } 156 | for line in lines { 157 | if line.contains('# ') { 158 | continue 159 | } 160 | 161 | if !line.contains(':') { 162 | continue 163 | } 164 | 165 | spl := line.split(':') 166 | 167 | if spl[0] == 'autohide_sidebar' { 168 | app.settings.autohide_sidebar = spl[1].trim_space().bool() 169 | } 170 | if spl[0] == 'round_ends' { 171 | app.settings.round_ends = spl[1].trim_space().bool() 172 | } 173 | if spl[0] == 'theme' { 174 | text := spl[1].trim_space() 175 | mut theme := ui.theme_by_name(text) 176 | app.win.set_theme(theme) 177 | app.set_theme_bg(text) 178 | app.settings.theme = text 179 | } 180 | if spl[0] == 'show_gridlines' { 181 | app.settings.show_gridlines = spl[1].trim_space().bool() 182 | } 183 | } 184 | } 185 | 186 | fn (mut app App) settings_save() ! { 187 | cfg_dir := get_cfg_dir() 188 | dir := os.join_path(cfg_dir, '.vpaint') 189 | file := os.join_path(dir, 'config.txt') 190 | 191 | if !os.exists(dir) { 192 | os.mkdir(dir) or { return err } 193 | } 194 | 195 | if !os.exists(file) { 196 | os.write_file(file, default_config.join('\n')) or { println(err) } 197 | } 198 | 199 | mut txt := [ 200 | '# VPaint Configuration File', 201 | 'autohide_sidebar: ${app.settings.autohide_sidebar}', 202 | 'theme: ${app.settings.theme}', 203 | 'round_ends: ${app.settings.round_ends}', 204 | 'show_gridlines: ${app.settings.show_gridlines}', 205 | ] 206 | os.write_file(file, txt.join('\n')) or { return err } 207 | 208 | if app.wasm_load_tick > 25 { 209 | wasm_save_files() 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/shapes.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import math 5 | 6 | // Line Tool 7 | struct LineTool { 8 | tool_name string = 'Line' 9 | mut: 10 | count int 11 | sx int = -1 12 | sy int = -1 13 | round bool = true 14 | } 15 | 16 | fn (mut this LineTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 17 | mut img := unsafe { &Image(a) } 18 | 19 | size := img.app.brush_size 20 | half_size := size / 2 21 | pix := img.zoom 22 | 23 | xpos := img.sx - (half_size * pix) 24 | ypos := img.sy - (half_size * pix) 25 | 26 | width := img.zoom + ((size - 1) * pix) 27 | 28 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 29 | 30 | // Draw lines instead of individual rects; 31 | // to reduce our drawing instructions. 32 | for i in 0 .. size { 33 | yy := ypos + (i * pix) 34 | xx := xpos + (i * pix) 35 | 36 | ctx.gg.draw_line(xpos, yy, xpos + width, yy, ctx.theme.accent_fill) 37 | ctx.gg.draw_line(xx, ypos, xx, ypos + width, ctx.theme.accent_fill) 38 | } 39 | } 40 | 41 | fn (mut this LineTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 42 | mut img := unsafe { &Image(a) } 43 | 44 | if this.sx == -1 { 45 | this.sx = img.mx 46 | this.sy = img.my 47 | } 48 | 49 | size := img.app.brush_size 50 | half_size := size / 2 51 | 52 | if this.sx != -1 { 53 | pp := bresenham(this.sx, this.sy, img.mx, img.my) 54 | for p in pp { 55 | aa, bb := img.get_point_screen_pos(p.x, p.y) 56 | g.gg.draw_rect_empty(aa - (half_size * img.zoom), bb - (half_size * img.zoom), 57 | img.zoom * size, img.zoom * size, g.theme.accent_fill) 58 | } 59 | } 60 | } 61 | 62 | fn (mut this LineTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 63 | mut img := unsafe { &Image(a) } 64 | 65 | size := img.app.brush_size 66 | half_size := size / 2 67 | 68 | round := this.round && size > 1 && img.app.settings.round_ends 69 | 70 | // Square Edges 71 | if this.sx != -1 && !round { 72 | pp := bresenham(this.sx, this.sy, img.mx, img.my) 73 | mut change := Multichange.new() 74 | for p in pp { 75 | for x in 0 .. size { 76 | for y in 0 .. size { 77 | img.set_raw(p.x + (x - half_size), p.y + (y - half_size), img.app.get_color(), mut 78 | change) 79 | } 80 | } 81 | } 82 | img.push(change) 83 | } 84 | 85 | // Round Edges 86 | if this.sx != -1 && round { 87 | pp := bresenham(this.sx, this.sy, img.mx, img.my) 88 | mut change := Multichange.new() 89 | for p in pp { 90 | for x in -half_size .. half_size { 91 | for y in -half_size .. half_size { 92 | if x * x + y * y <= half_size * half_size { 93 | img.set_raw(p.x + x, p.y + y, img.app.get_color(), mut change) 94 | } 95 | } 96 | } 97 | } 98 | 99 | // Draw circles at the start and end points to round the ends 100 | for x in -half_size .. half_size { 101 | for y in -half_size .. half_size { 102 | if x * x + y * y <= half_size * half_size { 103 | img.set_raw(this.sx + x, this.sy + y, img.app.get_color(), mut change) 104 | img.set_raw(img.mx + x, img.my + y, img.app.get_color(), mut change) 105 | } 106 | } 107 | } 108 | img.push(change) 109 | } 110 | 111 | img.refresh() 112 | 113 | // Reset 114 | this.sx = -1 115 | this.sy = -1 116 | } 117 | 118 | // Rect Tool 119 | struct RectTool { 120 | tool_name string = 'Rectangle' 121 | mut: 122 | count int 123 | sx int = -1 124 | sy int = -1 125 | } 126 | 127 | fn (mut this RectTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 128 | mut img := unsafe { &Image(a) } 129 | 130 | size := img.app.brush_size 131 | half_size := size / 2 132 | pix := img.zoom 133 | 134 | xpos := img.sx - (half_size * pix) 135 | ypos := img.sy - (half_size * pix) 136 | 137 | width := img.zoom + ((size - 1) * pix) 138 | 139 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 140 | 141 | // Draw lines instead of individual rects; 142 | // to reduce our drawing instructions. 143 | for i in 0 .. size { 144 | yy := ypos + (i * pix) 145 | xx := xpos + (i * pix) 146 | 147 | ctx.gg.draw_line(xpos, yy, xpos + width, yy, ctx.theme.accent_fill) 148 | ctx.gg.draw_line(xx, ypos, xx, ypos + width, ctx.theme.accent_fill) 149 | } 150 | } 151 | 152 | fn (mut this RectTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 153 | mut img := unsafe { &Image(a) } 154 | 155 | if this.sx == -1 { 156 | this.sx = img.mx 157 | this.sy = img.my 158 | } 159 | 160 | size := img.app.brush_size 161 | half_size := size / 2 162 | 163 | x1 := if this.sx < img.mx { this.sx } else { img.mx } 164 | y1 := if this.sy < img.my { this.sy } else { img.my } 165 | 166 | x2 := if this.sx < img.mx { img.mx } else { this.sx } 167 | y2 := if this.sy < img.my { img.my } else { this.sy } 168 | 169 | if this.sx != -1 { 170 | aa, bb := img.get_point_screen_pos(x1, y1) 171 | cc, dd := img.get_point_screen_pos(x2, y2) 172 | pix_size := img.zoom * size 173 | 174 | // Top, Bottom, Left, Right 175 | g.gg.draw_rect_filled(aa - (half_size * img.zoom), bb - (half_size * img.zoom), 176 | pix_size + (cc - aa), pix_size, g.theme.accent_fill) 177 | g.gg.draw_rect_filled(aa - (half_size * img.zoom), dd - (half_size * img.zoom), 178 | pix_size + (cc - aa), pix_size, g.theme.accent_fill) 179 | g.gg.draw_rect_filled(aa - (half_size * img.zoom), bb - (half_size * img.zoom), 180 | pix_size, pix_size + (dd - bb), g.theme.accent_fill) 181 | g.gg.draw_rect_filled(cc - (half_size * img.zoom), bb - (half_size * img.zoom), 182 | pix_size, pix_size + (dd - bb), g.theme.accent_fill) 183 | } 184 | } 185 | 186 | fn (mut this RectTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 187 | mut img := unsafe { &Image(a) } 188 | 189 | size := img.app.brush_size 190 | half_size := size / 2 191 | 192 | x1 := if this.sx < img.mx { this.sx } else { img.mx } 193 | y1 := if this.sy < img.my { this.sy } else { img.my } 194 | 195 | x2 := if this.sx < img.mx { img.mx } else { this.sx } 196 | y2 := if this.sy < img.my { img.my } else { this.sy } 197 | 198 | c := img.app.get_color() 199 | 200 | mut change := Multichange.new() 201 | 202 | if this.sx != -1 { 203 | for x in 0 .. size { 204 | for y in 0 .. size { 205 | for xx in x1 .. x2 { 206 | img.set_raw(xx + (x - half_size), y1 + (y - half_size), c, mut change) 207 | img.set_raw(xx + (x - half_size), y2 + (y - half_size), c, mut change) 208 | } 209 | for yy in y1 .. y2 { 210 | img.set_raw(x1 + (x - half_size), yy + (y - half_size), c, mut change) 211 | img.set_raw(x2 + (x - half_size), yy + (y - half_size), c, mut change) 212 | } 213 | } 214 | } 215 | } 216 | 217 | img.set_raw(x2, y2, c, mut change) 218 | img.push(change) 219 | img.refresh() 220 | 221 | // Reset 222 | this.sx = -1 223 | this.sy = -1 224 | } 225 | 226 | // Oval Tool 227 | struct OvalTool { 228 | tool_name string = 'Oval' 229 | mut: 230 | count int 231 | sx int = -1 232 | sy int = -1 233 | } 234 | 235 | fn (mut this OvalTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 236 | mut img := unsafe { &Image(a) } 237 | 238 | size := img.app.brush_size 239 | half_size := size / 2 240 | pix := img.zoom 241 | 242 | xpos := img.sx - (half_size * pix) 243 | ypos := img.sy - (half_size * pix) 244 | 245 | width := img.zoom + ((size - 1) * pix) 246 | 247 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 248 | 249 | // Draw lines instead of individual rects; 250 | // to reduce our drawing instructions. 251 | for i in 0 .. size { 252 | yy := ypos + (i * pix) 253 | xx := xpos + (i * pix) 254 | 255 | ctx.gg.draw_line(xpos, yy, xpos + width, yy, ctx.theme.accent_fill) 256 | ctx.gg.draw_line(xx, ypos, xx, ypos + width, ctx.theme.accent_fill) 257 | } 258 | } 259 | 260 | fn (mut this OvalTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 261 | mut img := unsafe { &Image(a) } 262 | 263 | if this.sx == -1 { 264 | this.sx = img.mx 265 | this.sy = img.my 266 | } 267 | 268 | size := img.app.brush_size 269 | 270 | x1 := if this.sx < img.mx { this.sx } else { img.mx } 271 | y1 := if this.sy < img.my { this.sy } else { img.my } 272 | 273 | x2 := if this.sx < img.mx { img.mx } else { this.sx } 274 | y2 := if this.sy < img.my { img.my } else { this.sy } 275 | 276 | if this.sx != -1 { 277 | pix_size := img.zoom * size 278 | half_pix := int(img.zoom * (size / 2)) 279 | 280 | center_x := (x1 + x2) / 2 281 | center_y := (y1 + y2) / 2 282 | radius_x := (x2 - x1) / 2 283 | radius_y := (y2 - y1) / 2 284 | 285 | for angle in 0 .. 360 { 286 | rad := f32(angle) * (f32(math.pi) / 180.0) 287 | x := int(radius_x * math.cos(rad)) 288 | y := int(radius_y * math.sin(rad)) 289 | 290 | aa, bb := img.get_point_screen_pos(center_x + x, center_y + y) 291 | g.gg.draw_rect_empty(aa - half_pix, bb - half_pix, pix_size, pix_size, g.theme.accent_fill) 292 | } 293 | } 294 | } 295 | 296 | fn (mut this OvalTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 297 | mut img := unsafe { &Image(a) } 298 | 299 | size := img.app.brush_size 300 | half_size := size / 2 301 | 302 | x1 := if this.sx < img.mx { this.sx } else { img.mx } 303 | y1 := if this.sy < img.my { this.sy } else { img.my } 304 | 305 | x2 := if this.sx < img.mx { img.mx } else { this.sx } 306 | y2 := if this.sy < img.my { img.my } else { this.sy } 307 | 308 | c := img.app.get_color() 309 | 310 | if this.sx != -1 { 311 | // Calculate the center and radius 312 | center_x := (x1 + x2) / 2 313 | center_y := (y1 + y2) / 2 314 | radius_x := (x2 - x1) / 2 315 | radius_y := (y2 - y1) / 2 316 | 317 | mut change := Multichange.new() 318 | 319 | mut last_x := 0 320 | mut last_y := 0 321 | 322 | for angle in 0 .. 360 { 323 | rad := f32(angle) * (f32(math.pi) / 180.0) 324 | x := int(radius_x * math.cos(rad)) 325 | y := int(radius_y * math.sin(rad)) 326 | 327 | px := center_x + x - half_size 328 | py := center_y + y - half_size 329 | 330 | if last_x != 0 && last_y != 0 { 331 | pp := bresenham(last_x, last_y, px, py) 332 | for p in pp { 333 | for xx in 0 .. size { 334 | for yy in 0 .. size { 335 | img.set_raw(p.x + xx, p.y + yy, c, mut change) 336 | } 337 | } 338 | } 339 | } else { 340 | for xx in 0 .. size { 341 | for yy in 0 .. size { 342 | img.set_raw(px + xx, py + yy, c, mut change) 343 | } 344 | } 345 | } 346 | 347 | last_x = px 348 | last_y = py 349 | } 350 | img.push(change) 351 | } 352 | 353 | img.refresh() 354 | 355 | // Reset 356 | this.sx = -1 357 | this.sy = -1 358 | } 359 | 360 | // Triangle Tool 361 | struct TriangleTool { 362 | tool_name string = 'Triangle' 363 | mut: 364 | count int 365 | sx int = -1 366 | sy int = -1 367 | } 368 | 369 | fn (mut this TriangleTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 370 | mut img := unsafe { &Image(a) } 371 | 372 | size := img.app.brush_size 373 | half_size := size / 2 374 | pix := img.zoom 375 | 376 | xpos := img.sx - (half_size * pix) 377 | ypos := img.sy - (half_size * pix) 378 | 379 | width := img.zoom + ((size - 1) * pix) 380 | 381 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 382 | 383 | // Draw lines instead of individual rects; 384 | // to reduce our drawing instructions. 385 | for i in 0 .. size { 386 | yy := ypos + (i * pix) 387 | xx := xpos + (i * pix) 388 | 389 | ctx.gg.draw_line(xpos, yy, xpos + width, yy, ctx.theme.accent_fill) 390 | ctx.gg.draw_line(xx, ypos, xx, ypos + width, ctx.theme.accent_fill) 391 | } 392 | } 393 | 394 | fn (mut this TriangleTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 395 | mut img := unsafe { &Image(a) } 396 | 397 | if this.sx == -1 { 398 | this.sx = img.mx 399 | this.sy = img.my 400 | } 401 | 402 | size := img.app.brush_size 403 | 404 | x_bottom_left := if img.mx >= this.sx { this.sx } else { img.mx } 405 | x_bottom_right := if img.mx >= this.sx { img.mx } else { this.sx } 406 | x_top_middle := (x_bottom_right + x_bottom_left) / 2 407 | 408 | y_bottom := if img.my >= this.sy { img.my } else { this.sy } 409 | y_top := if img.my >= this.sy { this.sy } else { img.my } 410 | 411 | if this.sx != -1 { 412 | // Draw triangle preview 413 | img.draw_line(x_top_middle, y_top, x_bottom_left, y_bottom, size, g) 414 | img.draw_line(x_bottom_left, y_bottom, x_bottom_right, y_bottom, size, g) 415 | img.draw_line(x_bottom_right, y_bottom, x_top_middle, y_top, size, g) 416 | } 417 | } 418 | 419 | fn (mut img Image) draw_line(x1 int, y1 int, x2 int, y2 int, size int, g &ui.GraphicsContext) { 420 | dx := abs(x2 - x1) 421 | dy := abs(y2 - y1) 422 | sx := if x1 < x2 { 1 } else { -1 } 423 | sy := if y1 < y2 { 1 } else { -1 } 424 | mut err := dx - dy 425 | 426 | mut x := x1 427 | mut y := y1 428 | 429 | // no_round := true // !img.app.settings.round_ends 430 | 431 | for { 432 | aa, bb := img.get_point_screen_pos(x, y) 433 | g.gg.draw_rect_empty(aa - ((size / 2) * img.zoom), bb - ((size / 2) * img.zoom), 434 | img.zoom * size, img.zoom * size, g.theme.accent_fill) 435 | 436 | if x == x2 && y == y2 { 437 | break 438 | } 439 | e2 := 2 * err 440 | if e2 > -dy { 441 | err -= dy 442 | x += sx 443 | } 444 | if e2 < dx { 445 | err += dx 446 | y += sy 447 | } 448 | } 449 | 450 | // Draw rounded edges 451 | // TODO 452 | // if !no_round { 453 | // draw_circle_filled(mut img, x1, y1, size / 2, c, mut change) 454 | // draw_circle_filled(mut img, x2, y2, size / 2, c, mut change) 455 | // } 456 | } 457 | 458 | fn (mut this TriangleTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 459 | mut img := unsafe { &Image(a) } 460 | 461 | size := img.app.brush_size 462 | 463 | c := img.app.get_color() 464 | 465 | mut change := Multichange.new() 466 | 467 | x_bottom_left := if img.mx >= this.sx { this.sx } else { img.mx } 468 | x_bottom_right := if img.mx >= this.sx { img.mx } else { this.sx } 469 | x_top_middle := (x_bottom_right + x_bottom_left) / 2 470 | 471 | y_bottom := if img.my >= this.sy { img.my } else { this.sy } 472 | y_top := if img.my >= this.sy { this.sy } else { img.my } 473 | 474 | if this.sx != -1 { 475 | // Draw the sides of the triangle with the specified size 476 | img.set_line(x_top_middle, y_top, x_bottom_left, y_bottom, c, size, mut change) 477 | img.set_line(x_bottom_left, y_bottom, x_bottom_right, y_bottom, c, size, mut change) 478 | img.set_line(x_bottom_right, y_bottom, x_top_middle, y_top, c, size, mut change) 479 | } 480 | 481 | img.push(change) 482 | img.refresh() 483 | 484 | // Reset 485 | this.sx = -1 486 | this.sy = -1 487 | } 488 | 489 | // Diamond Tool 490 | struct DiamondTool { 491 | tool_name string = 'Diamond' 492 | mut: 493 | count int 494 | sx int = -1 495 | sy int = -1 496 | } 497 | 498 | fn (mut this DiamondTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 499 | mut img := unsafe { &Image(a) } 500 | 501 | pix := img.zoom 502 | 503 | ctx.gg.draw_rounded_rect_filled(img.sx, img.sy, pix, pix, 4, ctx.theme.accent_fill) 504 | ctx.gg.draw_rounded_rect_filled(img.sx + 1, img.sy + 1, pix - 2, pix - 2, 4, img.app.get_color()) 505 | } 506 | 507 | fn (mut this DiamondTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 508 | mut img := unsafe { &Image(a) } 509 | 510 | if this.sx == -1 { 511 | this.sx = img.mx 512 | this.sy = img.my 513 | } 514 | 515 | size := img.app.brush_size 516 | 517 | x_left := if img.mx >= this.sx { this.sx } else { img.mx } 518 | x_right := if img.mx >= this.sx { img.mx } else { this.sx } 519 | x_middle := (x_right + x_left) / 2 520 | 521 | y_bottom := if img.my >= this.sy { img.my } else { this.sy } 522 | y_top := if img.my >= this.sy { this.sy } else { img.my } 523 | y_middle := (y_top + y_bottom) / 2 524 | 525 | if this.sx != -1 { 526 | // Draw diamond preview 527 | img.draw_line(x_middle, y_top, x_left, y_middle, size, g) 528 | img.draw_line(x_left, y_middle, x_middle, y_bottom, size, g) 529 | img.draw_line(x_middle, y_bottom, x_right, y_middle, size, g) 530 | img.draw_line(x_right, y_middle, x_middle, y_top, size, g) 531 | } 532 | } 533 | 534 | fn (mut this DiamondTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 535 | mut img := unsafe { &Image(a) } 536 | 537 | size := img.app.brush_size 538 | 539 | c := img.app.get_color() 540 | 541 | mut change := Multichange.new() 542 | 543 | x_left := if img.mx >= this.sx { this.sx } else { img.mx } 544 | x_right := if img.mx >= this.sx { img.mx } else { this.sx } 545 | x_middle := (x_right + x_left) / 2 546 | 547 | y_bottom := if img.my >= this.sy { img.my } else { this.sy } 548 | y_top := if img.my >= this.sy { this.sy } else { img.my } 549 | y_middle := (y_top + y_bottom) / 2 550 | 551 | if this.sx != -1 { 552 | // Draw the sides of the diamond with the specified size 553 | img.set_line(x_middle, y_top, x_left, y_middle, c, size, mut change) 554 | img.set_line(x_left, y_middle, x_middle, y_bottom, c, size, mut change) 555 | img.set_line(x_middle, y_bottom, x_right, y_middle, c, size, mut change) 556 | img.set_line(x_right, y_middle, x_middle, y_top, c, size, mut change) 557 | } 558 | 559 | img.push(change) 560 | img.refresh() 561 | 562 | // Reset 563 | this.sx = -1 564 | this.sy = -1 565 | } 566 | -------------------------------------------------------------------------------- /src/sidebar.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | 5 | fn (mut app App) sidebar_autohide_draw(w int) { 6 | move := 3 7 | hidden_size := 6 8 | 9 | if app.win.mouse_x < app.sidebar.width + (hidden_size * 2) && app.win.mouse_y > app.sidebar.ry { 10 | if app.sidebar.width < w { 11 | app.sidebar.width += move 12 | app.sidebar.children[0].set_hidden(false) 13 | } 14 | } else { 15 | if app.sidebar.width > hidden_size { 16 | app.sidebar.width -= move 17 | } else { 18 | app.sidebar.width = hidden_size 19 | app.sidebar.children[0].set_hidden(true) 20 | } 21 | } 22 | app.sidebar.children[0].x = app.sidebar.width - w 23 | } 24 | 25 | fn sidebar_draw_event(mut e ui.DrawEvent) { 26 | // webasm build works better without closures 27 | mut app := e.ctx.win.get[&App]('app') 28 | 29 | $if emscripten ? { 30 | if app.wasm_load_tick < 25 { 31 | app.wasm_load_tick += 1 32 | } 33 | 34 | if app.wasm_load_tick > 5 && app.wasm_load_tick < 10 { 35 | println('Wasm detected. Loading local storage.') 36 | wasm_load_files() 37 | app.wasm_load_tick = 10 38 | } 39 | 40 | if app.wasm_load_tick > 20 && app.wasm_load_tick < 26 { 41 | // Load Settings after a few ticks 42 | // for some reason, Emscripten will crash if FS is called without this. 43 | println('Wasm detected. Reloading settings...') 44 | app.settings_load() or {} 45 | app.wasm_load_tick = 28 46 | app.settings_save() or {} 47 | } 48 | } 49 | 50 | h := app.sidebar.height 51 | w := if h > 180 { 46 } else { 104 } 52 | 53 | color := e.ctx.theme.textbox_background 54 | e.ctx.gg.draw_rect_filled(0, app.sidebar.ry, app.sidebar.width, app.sidebar.height, 55 | color) 56 | 57 | if app.settings.autohide_sidebar { 58 | app.sidebar_autohide_draw(w) 59 | return 60 | } 61 | 62 | if app.sidebar.children[0].hidden { 63 | app.sidebar.children[0].set_hidden(false) 64 | } 65 | 66 | app.sidebar.width = w 67 | app.sidebar.children[0].x = 0 68 | app.sidebar.children[0].width = w 69 | } 70 | 71 | // Shapes 72 | const shape_labels = ['Line', 'Rectangle', 'Oval', 'Triangle', 'Diamond'] // , 'Arrow Up', 'Arrow Right', 'Arrow Down'] 73 | const shape_uicons = ['\ue937', '\ue003', '\uea57', '\uee4a', '\uE91A', '\uEA33'] // , '\uEA35', '\uEA39', '\uEA37'] 74 | 75 | fn (mut app App) set_tool_by_name(name string) { 76 | match name { 77 | 'Select' { 78 | app.tool = &SelectTool{} 79 | } 80 | 'Pencil' { 81 | app.tool = &PencilTool{} 82 | } 83 | 'Fill', 'Fillcan' { 84 | app.tool = &FillTool{} 85 | } 86 | 'Drag' { 87 | app.tool = &DragTool{} 88 | } 89 | 'Airbrush' { 90 | app.tool = &AirbrushTool{} 91 | } 92 | 'Dropper', 'Eye Dropper' { 93 | app.tool = &DropperTool{} 94 | } 95 | 'WidePencil' { 96 | app.tool = &CustomPencilTool{ 97 | width: 0 98 | height: 2 99 | } 100 | } 101 | 'CustomPencil' { 102 | app.tool = &CustomPencilTool{} 103 | } 104 | 'Line' { 105 | app.tool = &LineTool{} 106 | } 107 | 'Rectangle' { 108 | app.tool = &RectTool{} 109 | } 110 | 'Oval', 'Circle' { 111 | app.tool = &OvalTool{} 112 | } 113 | 'Triangle' { 114 | app.tool = &TriangleTool{} 115 | } 116 | 'Diamond' { 117 | app.tool = &DiamondTool{} 118 | } 119 | else { 120 | dump(name) 121 | app.tool = &PencilTool{} 122 | } 123 | } 124 | } 125 | 126 | fn (mut app App) make_sidebar_test() &ui.NavPane { 127 | mut p := ui.NavPane.new( 128 | collapsed: true 129 | ) 130 | 131 | /* 132 | mut b0 := app.icon_btn_1(0, 0, 'Select') 133 | mut b1 := app.icon_btn_1(1, 0, 'Pencil') 134 | mut b2 := app.icon_btn_1(2, 0, 'Fill') 135 | mut b3 := app.icon_btn_1(3, 0, 'Drag') 136 | mut b4 := app.icon_btn_1(4, 0, 'Resize Canvas') 137 | mut b5 := app.icon_btn_1(5, 0, 'Airbrush') 138 | mut b6 := app.icon_btn_1(6, 0, 'Dropper') 139 | mut b7 := app.icon_btn_1(7, 0, 'WidePencil') 140 | */ 141 | 142 | names := ['Select', 'Pencil', 'Fill', 'Drag', 'Resize Canvas', 'Airbrush', 'Dropper', 143 | 'WidePencil'] 144 | 145 | for name in names { 146 | mut item := ui.NavPaneItem.new( 147 | icon: 'A' 148 | text: name 149 | ) 150 | p.add_child(item) 151 | } 152 | 153 | return p 154 | } 155 | 156 | fn (mut app App) make_sidebar() { 157 | // Sidebar 158 | app.sidebar.subscribe_event('draw', sidebar_draw_event) 159 | 160 | // icons 161 | img_icon_file := $embed_file('assets/tools.png') 162 | mut gg := app.win.gg 163 | gg_im := gg.create_image_from_byte_array(img_icon_file.to_bytes()) or { panic(err) } 164 | cim := gg.cache_image(gg_im) 165 | app.win.graphics_context.icon_cache['icons-3'] = cim 166 | 167 | /* 168 | img_sele_file := $embed_file('assets/select.png') 169 | img_pencil_file := $embed_file('assets/pencil-tip.png') 170 | img_fill_file := $embed_file('assets/fill-can.png') 171 | img_drag_file := $embed_file('assets/icons8-drag-32.png') 172 | img_resize_file := $embed_file('assets/resize.png') 173 | img_airbrush_file := $embed_file('assets/icons8-paint-sprayer-32.png') 174 | img_dropper_file := $embed_file('assets/color-dropper.png') 175 | img_wide_file := $embed_file('assets/icons8-pencil-drawing-32.png') 176 | */ 177 | 178 | // Buttons 179 | mut b0 := app.icon_btn_1(0, 0, 'Select') 180 | mut b1 := app.icon_btn_1(1, 0, 'Pencil') 181 | mut b2 := app.icon_btn_1(2, 0, 'Fill') 182 | mut b3 := app.icon_btn_1(3, 0, 'Drag') 183 | mut b4 := app.icon_btn_1(4, 0, 'Resize Canvas') 184 | mut b5 := app.icon_btn_1(5, 0, 'Airbrush') 185 | mut b6 := app.icon_btn_1(6, 0, 'Dropper') 186 | mut b7 := app.icon_btn_1(7, 0, 'WidePencil') 187 | 188 | b4.subscribe_event('mouse_up', fn [mut app] (mut e ui.MouseEvent) { 189 | app.show_resize_modal(app.canvas.w, app.canvas.h) 190 | }) 191 | 192 | mut p := ui.Panel.new( 193 | layout: ui.FlowLayout.new( 194 | hgap: 1 195 | vgap: 2 196 | ) 197 | ) 198 | 199 | mut group := ui.buttongroup[ui.Button]() 200 | 201 | for child in [b0, b1, b2, b3, b4, b5, b6, b7] { 202 | p.add_child(child) 203 | group.add(child) 204 | } 205 | 206 | group.subscribe_event('mouse_up', app.group_clicked) 207 | group.setup() 208 | 209 | app.sidebar.add_child(p) 210 | } 211 | 212 | fn draw_btn(mut e ui.DrawEvent) { 213 | mut btn := e.target 214 | if mut btn is ui.Button { 215 | btn.set_area_filled(e.target.is_selected) 216 | } 217 | } 218 | 219 | fn after_draw_btn(mut e ui.DrawEvent) { 220 | if e.target.is_selected { 221 | mut btn := e.target 222 | for i in 1 .. 2 { 223 | x := btn.x + i 224 | y := btn.y + i 225 | w := btn.width - (2 * i) 226 | h := btn.height - (2 * i) 227 | e.ctx.gg.draw_rect_empty(x, y, w, h, e.ctx.theme.accent_fill) 228 | } 229 | } 230 | } 231 | 232 | fn (mut app App) group_clicked(mut e ui.MouseEvent) { 233 | } 234 | 235 | fn (mut app App) icon_btn_1(xp int, yp int, name string) &ui.Button { 236 | mut btn := ui.Button.new( 237 | icon: -3 238 | ) 239 | 240 | btn.icon_info = ui.ButtonIconInfo{ 241 | id: 'icons-3' 242 | atlas_size: 32 243 | x: xp 244 | y: yp 245 | skip_text: true 246 | } 247 | 248 | btn.set_bounds(0, 0, 42, 32) 249 | 250 | btn.icon_width = 32 251 | btn.icon_height = 32 252 | 253 | btn.set_area_filled(false) 254 | btn.border_radius = -1 255 | 256 | btn.extra = name // tool.tool_name 257 | btn.text = name 258 | 259 | btn.subscribe_event('after_draw', after_draw_btn) 260 | btn.subscribe_event('draw', draw_btn) 261 | 262 | btn.subscribe_event('mouse_up', fn [mut app] (mut e ui.MouseEvent) { 263 | // Note: debug this. 264 | // seems my closure impl for emscripten always returns 265 | // the last 'name' instead of the real name. 266 | app.set_tool_by_name(e.target.text) 267 | }) 268 | return btn 269 | } 270 | 271 | fn (mut app App) icon_btn(data []u8, name string) &ui.Button { 272 | mut gg := app.win.gg 273 | gg_im := gg.create_image_from_byte_array(data) or { panic(err) } 274 | cim := gg.cache_image(gg_im) 275 | mut btn := ui.Button.new(icon: cim) 276 | btn.set_bounds(0, 0, 42, 32) 277 | btn.icon_width = 32 278 | 279 | btn.set_area_filled(false) 280 | btn.border_radius = -1 281 | 282 | btn.extra = name // tool.tool_name 283 | btn.text = name 284 | 285 | btn.subscribe_event('after_draw', after_draw_btn) 286 | btn.subscribe_event('draw', draw_btn) 287 | 288 | btn.subscribe_event('mouse_up', fn [mut app] (mut e ui.MouseEvent) { 289 | // Note: debug this. 290 | // seems my closure impl for emscripten always returns 291 | // the last 'name' instead of the real name. 292 | app.set_tool_by_name(e.target.text) 293 | }) 294 | return btn 295 | } 296 | 297 | fn (mut app App) icon_btn_old(data []u8, tool &Tool) &ui.Button { 298 | mut gg := app.win.gg 299 | gg_im := gg.create_image_from_byte_array(data) or { panic(err) } 300 | cim := gg.cache_image(gg_im) 301 | mut btn := ui.Button.new(icon: cim) 302 | btn.set_bounds(2, 0, 46, 32) 303 | btn.icon_width = 32 304 | 305 | btn.set_area_filled(false) 306 | btn.border_radius = -1 307 | 308 | btn.extra = tool.tool_name 309 | 310 | btn.subscribe_event('mouse_up', fn [mut app, tool] (mut e ui.MouseEvent) { 311 | app.tool = unsafe { tool } 312 | }) 313 | return btn 314 | } 315 | -------------------------------------------------------------------------------- /src/statusbar.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | 5 | fn statusbar_draw_event(mut e ui.DrawEvent) { 6 | mut win := e.ctx.win 7 | mut app := win.get[&App]('app') 8 | ws := win.gg.window_size() 9 | mut sb := app.status_bar 10 | sb.width = ws.width 11 | sb.height = 32 12 | 13 | win.gg.draw_rect_filled(sb.x, sb.y, sb.width, sb.height, win.theme.menubar_background) 14 | // win.gg.draw_rect_empty(sb.x, sb.y, sb.width, 1, win.theme.dropdown_border) 15 | // win.gg.draw_rect_empty(sb.x, 26, sb.width, 1, win.theme.dropdown_border) 16 | } 17 | 18 | fn (mut app App) make_status_bar(window &ui.Window) &ui.Panel { 19 | mut sb := ui.Panel.new( 20 | layout: ui.BorderLayout.new(vgap: 4) 21 | ) 22 | 23 | sb.subscribe_event('draw', statusbar_draw_event) 24 | 25 | mut zoom_inc := app.zoom_btn(1) 26 | zoom_inc.subscribe_event('mouse_up', app.on_zoom_inc) 27 | zoom_inc.set_bounds(1, 0, 40, 26) 28 | 29 | mut zoom_dec := app.zoom_btn(0) 30 | zoom_dec.subscribe_event('mouse_up', app.on_zoom_dec) 31 | zoom_dec.set_bounds(4, 0, 40, 26) 32 | 33 | mut status := ui.Label.new( 34 | text: 'status' 35 | vertical_align: .middle 36 | ) 37 | app.stat_lbl = status 38 | 39 | mut zoom_lbl := ui.Label.new(text: '100%') 40 | 41 | zoom_lbl.subscribe_event('draw', fn (mut e ui.DrawEvent) { 42 | mut com := e.target 43 | mut app := e.ctx.win.get[&App]('app') 44 | zoom := int(app.canvas.get_zoom() * 100) 45 | com.text = '${zoom}%' 46 | if mut com is ui.Label { 47 | com.center_text_y = true 48 | com.width = e.ctx.text_width(com.text) 49 | com.height = com.parent.height 50 | } 51 | }) 52 | 53 | status.subscribe_event('draw', stat_lbl_draw_event) 54 | 55 | mut stat_btn := ui.Button.new( 56 | text: '\uE90E' 57 | ) 58 | status.set_bounds(0, 0, 0, 24) 59 | stat_btn.set_bounds(0, 0, 24, 24) 60 | stat_btn.font = 1 61 | 62 | stat_btn.subscribe_event('mouse_up', fn (mut e ui.MouseEvent) { 63 | mut app := e.ctx.win.get[&App]('app') 64 | app.show_prop_modal(e.ctx) 65 | }) 66 | 67 | mut tool_select := ui.Selectbox.new( 68 | text: 'Pencil' 69 | items: [ 70 | 'Select', 71 | 'Pencil', 72 | 'Fillcan', 73 | 'Drag', 74 | 'Airbrush', 75 | 'Eye Dropper', 76 | 'Line', 77 | 'Rectangle', 78 | 'Oval', 79 | 'Triangle', 80 | ] 81 | ) 82 | 83 | tool_select.subscribe_event('item_change', box_change_fn) 84 | tool_select.subscribe_event('draw', tool_box_draw_event) 85 | tool_select.set_bounds(0, 0, 90, 25) 86 | 87 | mut west_panel := ui.Panel.new( 88 | layout: ui.BoxLayout.new(vgap: 0, hgap: 5) 89 | ) 90 | 91 | west_panel.set_bounds(-5, 0, 250, 0) 92 | 93 | west_panel.add_child(stat_btn) 94 | west_panel.add_child(tool_select) 95 | west_panel.add_child(status) 96 | 97 | mut zp := ui.Panel.new( 98 | layout: ui.BoxLayout.new(vgap: 0, hgap: 5) 99 | ) 100 | sb.add_child_with_flag(west_panel, ui.borderlayout_west) 101 | zp.add_child(zoom_lbl) 102 | zp.add_child(zoom_dec) 103 | zp.add_child(zoom_inc) 104 | sb.add_child_with_flag(zp, ui.borderlayout_east) 105 | return sb 106 | } 107 | 108 | fn box_change_fn(mut e ui.ItemChangeEvent) { 109 | mut app := e.ctx.win.get[&App]('app') 110 | app.set_tool_by_name(e.new_val) 111 | } 112 | 113 | fn tool_box_draw_event(mut e ui.DrawEvent) { 114 | app := e.ctx.win.get[&App]('app') 115 | e.target.text = '${app.tool.tool_name}' 116 | 117 | tw := e.ctx.text_width(e.target.text) + 32 118 | 119 | if e.target.width < tw { 120 | e.target.width = tw 121 | } 122 | } 123 | 124 | fn stat_lbl_draw_event(mut e ui.DrawEvent) { 125 | app := e.ctx.win.get[&App]('app') 126 | mouse_details := '(${app.canvas.mx}, ${app.canvas.my})' 127 | mut com := e.target 128 | 129 | ww := e.ctx.gg.window_size().width 130 | if ww < 440 { 131 | com.text = '${app.canvas.w}x${app.canvas.h} / ${mouse_details}' 132 | } else { 133 | com.text = '${app.canvas.w}x${app.canvas.h} / ${app.data.file_size} / m: ${mouse_details}' 134 | } 135 | 136 | if mut com is ui.Label { 137 | com.width = e.ctx.text_width(com.text) 138 | com.height = 24 139 | } 140 | } 141 | 142 | fn (mut app App) zoom_btn(val int) &ui.Button { 143 | txt := if val == 0 { '\ue989' } else { '\ue988' } 144 | mut btn := ui.Button.new(text: txt) 145 | btn.font = 1 146 | btn.border_radius = 8 147 | return btn 148 | } 149 | 150 | fn (mut app App) on_zoom_inc(mut e ui.MouseEvent) { 151 | zoom := app.canvas.get_zoom() 152 | new_zoom := if zoom >= 1 { zoom + 1 } else { zoom + .25 } 153 | 154 | if zoom < .25 { 155 | app.canvas.set_zoom(.25) 156 | return 157 | } 158 | 159 | app.canvas.set_zoom(new_zoom) 160 | } 161 | 162 | fn (mut app App) on_zoom_dec(mut e ui.MouseEvent) { 163 | zoom := app.canvas.get_zoom() 164 | new_zoom := if zoom > 1 { zoom - 1 } else { zoom - .25 } 165 | 166 | if new_zoom < .25 { 167 | app.canvas.set_zoom(.15) 168 | return 169 | } 170 | 171 | app.canvas.set_zoom(new_zoom) 172 | } 173 | -------------------------------------------------------------------------------- /src/tools.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gx 5 | import math 6 | // import rand { intn } 7 | 8 | // Use rand from stdlib 9 | // Saves 4KB on wasm build 10 | fn C.rand() int 11 | fn intn(max int) int { 12 | return C.rand() % (max + 1) 13 | } 14 | 15 | // Tools 16 | interface Tool { 17 | tool_name string 18 | mut: 19 | draw_hover_fn(voidptr, &ui.GraphicsContext) 20 | draw_down_fn(voidptr, &ui.GraphicsContext) 21 | draw_click_fn(voidptr, &ui.GraphicsContext) 22 | } 23 | 24 | // Pencil Tool 25 | struct PencilTool { 26 | tool_name string = 'Pencil' 27 | mut: 28 | count int 29 | change Multichange = Multichange.new() 30 | } 31 | 32 | fn (mut this PencilTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 33 | mut img := unsafe { &Image(a) } 34 | 35 | size := img.app.brush_size 36 | half_size := size / 2 37 | pix := img.zoom 38 | 39 | xpos := img.sx - (half_size * pix) 40 | ypos := img.sy - (half_size * pix) 41 | 42 | width := img.zoom + ((size - 1) * pix) 43 | 44 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 45 | 46 | // Draw lines instead of individual rects; 47 | // to reduce our drawing instructions. 48 | for i in 0 .. size { 49 | yy := ypos + (i * pix) 50 | xx := xpos + (i * pix) 51 | 52 | ctx.gg.draw_line(xpos, yy, xpos + width, yy, ctx.theme.accent_fill) 53 | ctx.gg.draw_line(xx, ypos, xx, ypos + width, ctx.theme.accent_fill) 54 | } 55 | } 56 | 57 | fn (mut this PencilTool) draw_down_fn(a voidptr, g &ui.GraphicsContext) { 58 | mut img := unsafe { &Image(a) } 59 | 60 | size := img.app.brush_size 61 | half_size := size / 2 62 | 63 | if img.last_x != -1 { 64 | if img.app.settings.round_ends { 65 | img.set_line(img.last_x, img.last_y, img.mx, img.my, img.app.get_color(), 66 | size, mut this.change) 67 | } else { 68 | pp := bresenham(img.last_x, img.last_y, img.mx, img.my) 69 | for p in pp { 70 | for x in 0 .. size { 71 | for y in 0 .. size { 72 | img.set_raw(p.x + (x - half_size), p.y + (y - half_size), img.app.get_color(), mut 73 | this.change) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | img.last_x = img.mx 81 | img.last_y = img.my 82 | img.refresh() 83 | } 84 | 85 | fn (mut this PencilTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 86 | mut img := unsafe { &Image(a) } 87 | img.push(this.change) 88 | this.change = Multichange.new() 89 | } 90 | 91 | // Drag Tool 92 | struct DragTool { 93 | tool_name string = 'Drag' 94 | mut: 95 | dx int = -1 96 | dy int 97 | sx f32 98 | sy f32 99 | } 100 | 101 | fn (mut this DragTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 102 | } 103 | 104 | fn (mut this DragTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 105 | mut img := unsafe { &Image(a) } 106 | 107 | if this.dx == -1 { 108 | this.dx = img.mx 109 | this.dy = img.my 110 | this.sx, this.sy = img.get_point_screen_pos(this.dx, this.dy) 111 | } 112 | 113 | // TODO: 114 | // if app.selection_area {} 115 | 116 | sx, sy := img.get_point_screen_pos(img.mx, img.my) 117 | 118 | diff_x := sx - this.sx 119 | diff_y := sy - this.sy 120 | 121 | sdx := if diff_x < 0 { -4 } else { 4 } 122 | sdy := if diff_y < 0 { -4 } else { 4 } 123 | 124 | if math.abs(diff_x) > img.zoom { 125 | img.app.sv.scroll_x += sdx 126 | } 127 | if math.abs(diff_y) > img.zoom { 128 | img.app.sv.scroll_i += sdy 129 | } 130 | } 131 | 132 | fn (mut this DragTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 133 | this.dx = -1 134 | this.dy = -1 135 | // sapp.set_mouse_cursor(.default) 136 | } 137 | 138 | // Pencil Tool 139 | struct AirbrushTool { 140 | tool_name string = 'Airbrush' 141 | mut: 142 | change Multichange = Multichange.new() 143 | } 144 | 145 | fn (mut this AirbrushTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 146 | mut img := unsafe { &Image(a) } 147 | 148 | size := img.app.brush_size 149 | half_size := size / 2 150 | pix := img.zoom 151 | 152 | for x in 0 .. size { 153 | for y in 0 .. size { 154 | xpos := img.sx + (x * pix) - (half_size * pix) 155 | ypos := img.sy + (y * pix) - (half_size * pix) 156 | rand_int := intn(size) 157 | if rand_int == 0 { 158 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, img.zoom, img.zoom, 1, ctx.theme.accent_fill) 159 | } 160 | } 161 | } 162 | ctx.gg.draw_rounded_rect_empty(img.sx, img.sy, img.zoom, img.zoom, 1, ctx.theme.accent_fill) 163 | } 164 | 165 | fn (mut this AirbrushTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 166 | mut img := unsafe { &Image(a) } 167 | 168 | size := img.app.brush_size 169 | half_size := size / 2 170 | 171 | for x in 0 .. size { 172 | for y in 0 .. size { 173 | rand_int := intn(size) 174 | if rand_int == 0 { 175 | img.set_raw(img.mx + (x - half_size), img.my + (y - half_size), img.app.get_color(), mut 176 | this.change) 177 | } 178 | } 179 | } 180 | 181 | img.set_raw(img.mx, img.my, img.app.get_color(), mut this.change) 182 | img.refresh() 183 | } 184 | 185 | fn (mut this AirbrushTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 186 | mut img := unsafe { &Image(a) } 187 | img.push(this.change) 188 | this.change = Multichange.new() 189 | } 190 | 191 | // Dropper Tool 192 | struct DropperTool { 193 | tool_name string = 'Eye Dropper' 194 | } 195 | 196 | fn (mut this DropperTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 197 | mut img := unsafe { &Image(a) } 198 | 199 | color := img.get(img.mx, img.my) 200 | 201 | width := if img.zoom > 4 { img.zoom * 4 } else { 16 } 202 | xpos := img.sx + width 203 | ypos := img.sy + width 204 | 205 | ctx.gg.draw_rounded_rect_filled(xpos, ypos, width, width, 1, color) 206 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, width, 1, ctx.theme.accent_fill) 207 | str := 'RGBA: ${color.r}, ${color.g}, ${color.b}, ${color.a}' 208 | 209 | ctx.gg.draw_text(int(xpos), int(ypos), str, gx.TextCfg{ 210 | size: 12 211 | }) 212 | 213 | ctx.gg.set_text_cfg() 214 | 215 | mut win := ctx.win 216 | win.tooltip = str 217 | } 218 | 219 | fn (mut this DropperTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 220 | mut img := unsafe { &Image(a) } 221 | 222 | color := img.get(img.mx, img.my) 223 | img.app.set_color(color) 224 | } 225 | 226 | fn (mut this DropperTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 227 | } 228 | 229 | // Custom Pencil Tool 230 | struct CustomPencilTool { 231 | tool_name string = 'Custom Pencil' 232 | mut: 233 | width int = 8 234 | height int = 2 235 | change Multichange = Multichange.new() 236 | } 237 | 238 | fn (mut this CustomPencilTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 239 | mut img := unsafe { &Image(a) } 240 | 241 | size := if this.width > 0 { this.width } else { img.app.brush_size } 242 | height := if this.height > 0 { this.height } else { img.app.brush_size } 243 | 244 | q_size := height / 2 245 | pix := img.zoom 246 | 247 | xpos := img.sx - (size * pix) 248 | ypos := img.sy - (q_size * pix) 249 | 250 | width := img.zoom + (((size * 2) - 1) * pix) 251 | hei := img.zoom + ((height - 1) * pix) 252 | 253 | ctx.gg.draw_rounded_rect_empty(xpos, ypos, width, hei, 1, ctx.theme.accent_fill) 254 | } 255 | 256 | fn (mut this CustomPencilTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 257 | mut img := unsafe { &Image(a) } 258 | 259 | size := if this.width > 0 { this.width } else { img.app.brush_size } 260 | height := if this.height > 0 { this.height } else { img.app.brush_size } 261 | half_size := size / 2 262 | 263 | if img.last_x != -1 { 264 | pp := bresenham(img.last_x, img.last_y, img.mx, img.my) 265 | for p in pp { 266 | for x in -half_size .. size + half_size { 267 | for y in 0 .. height { 268 | img.set_raw(p.x + (x - half_size), p.y + (y - (height / 2)), img.app.get_color(), mut 269 | this.change) 270 | } 271 | } 272 | } 273 | } 274 | 275 | img.last_x = img.mx 276 | img.last_y = img.my 277 | img.refresh() 278 | } 279 | 280 | fn (mut this CustomPencilTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 281 | mut img := unsafe { &Image(a) } 282 | img.push(this.change) 283 | this.change = Multichange.new() 284 | } 285 | 286 | // Fill Tool 287 | struct FillTool { 288 | tool_name string = 'Fillcan' 289 | mut: 290 | color gx.Color 291 | img &Image = unsafe { nil } 292 | count int 293 | next []Point 294 | change Multichange = Multichange.new() 295 | } 296 | 297 | fn (mut this FillTool) draw_hover_fn(a voidptr, ctx &ui.GraphicsContext) { 298 | mut img := unsafe { &Image(a) } 299 | ctx.gg.draw_rounded_rect_empty(img.sx, img.sy, img.zoom, img.zoom, 1, gx.blue) 300 | 301 | for p in this.next { 302 | if p.x != -1 { 303 | this.fill_points(p.x, p.y) 304 | } 305 | } 306 | if this.next.len > 0 { 307 | this.next.clear() 308 | unsafe { this.next.free() } 309 | unsafe { free(this.next) } 310 | this.next = []Point{} 311 | this.img.refresh() 312 | } 313 | } 314 | 315 | fn (mut this FillTool) draw_down_fn(a voidptr, b &ui.GraphicsContext) { 316 | this.next.clear() 317 | this.count = 0 318 | 319 | mut img := unsafe { &Image(a) } 320 | this.img = img 321 | 322 | x := img.mx 323 | y := img.my 324 | 325 | down_color := img.get(x, y) 326 | 327 | if down_color == img.app.get_color() { 328 | // If same color return 329 | return 330 | } 331 | 332 | this.color = down_color 333 | this.fill_points(x, y) 334 | 335 | img.set_raw(img.mx, img.my, img.app.get_color(), mut this.change) 336 | img.refresh() 337 | } 338 | 339 | fn (mut this FillTool) fill_points(x int, y int) { 340 | if x < 0 || y < 0 { 341 | return 342 | } 343 | 344 | mut img := this.img 345 | 346 | out_of_bounds := x >= img.w || y >= img.h 347 | same_color := this.color == img.app.get_color() 348 | 349 | if out_of_bounds || same_color { 350 | return 351 | } 352 | 353 | this.fill_point_(x, y) 354 | } 355 | 356 | fn (mut this FillTool) fill_point_(x int, y int) { 357 | this.fill_point(x, y - 1) 358 | this.fill_point(x, y + 1) 359 | this.fill_point(x - 1, y) 360 | this.fill_point(x + 1, y) 361 | } 362 | 363 | fn (mut this FillTool) fill_point(x int, y int) { 364 | color := this.img.get(x, y) 365 | if color == this.color { 366 | this.img.set_raw(x, y, this.img.app.get_color(), mut this.change) 367 | this.next << Point{x, y} 368 | } 369 | } 370 | 371 | fn (mut this FillTool) draw_click_fn(a voidptr, b &ui.GraphicsContext) { 372 | mut img := unsafe { &Image(a) } 373 | img.push(this.change) 374 | this.change = Multichange.new() 375 | } 376 | -------------------------------------------------------------------------------- /src/util.v: -------------------------------------------------------------------------------- 1 | // Excerpt from https://github.com/vlang/ui/blob/master/src/extra_draw.v 2 | module main 3 | 4 | import gx 5 | import math 6 | 7 | fn wasm_keyboard_show(val bool) { 8 | $if emscripten ? { 9 | value := if val { '"keyboard-show"' } else { 'keyboard-hide' } 10 | C.emscripten_run_script(cstr('iui.trigger = ' + value)) 11 | } 12 | } 13 | 14 | // h, s, l in [0,1] 15 | pub fn hsv_to_rgb(h f64, s f64, v f64) gx.Color { 16 | c := v * s 17 | x := c * (1.0 - math.abs(math.fmod(h * 6.0, 2.0) - 1.0)) 18 | m := v - c 19 | mut r, mut g, mut b := 0.0, 0.0, 0.0 20 | h6 := h * 6.0 21 | if h6 < 1.0 { 22 | r, g = c, x 23 | } else if h6 < 2.0 { 24 | r, g = x, c 25 | } else if h6 < 3.0 { 26 | g, b = c, x 27 | } else if h6 < 4.0 { 28 | g, b = x, c 29 | } else if h6 < 5.0 { 30 | r, b = x, c 31 | } else { 32 | r, b = c, x 33 | } 34 | return gx.rgb(u8((r + m) * 255.0), u8((g + m) * 255.0), u8((b + m) * 255.0)) 35 | } 36 | 37 | pub fn rgb_to_hsv(col gx.Color) (f64, f64, f64) { 38 | r, g, b := f64(col.r) / 255.0, f64(col.g) / 255.0, f64(col.b) / 255.0 39 | v, m := f64_max(f64_max(r, g), b), -f64_max(f64_max(-r, -g), -b) 40 | d := v - m 41 | mut h, mut s := 0.0, 0.0 42 | if v == m { 43 | h = 0 44 | } else if v == r { 45 | if g > b { 46 | h = ((g - b) / d) / 6.0 47 | } else { 48 | h = (6.0 - (g - b) / d) / 6 49 | } 50 | } else if v == g { 51 | h = ((b - r) / d + 2.0) / 6.0 52 | } else if v == b { 53 | h = ((r - g) / d + 4.0) / 6.0 54 | } 55 | 56 | if v != 0 { 57 | s = d / v 58 | } 59 | 60 | // mirror correction 61 | if h > 1.0 { 62 | h = 2.0 - h 63 | } 64 | 65 | return h, s, v 66 | } 67 | 68 | fn abs(a int) int { 69 | if a < 0 { 70 | return -a 71 | } 72 | return a 73 | } 74 | 75 | struct Point { 76 | x int 77 | y int 78 | } 79 | 80 | @[unsafe] 81 | fn (data &Point) free() { 82 | // ... 83 | unsafe { 84 | free(data.x) 85 | free(data.y) 86 | free(data) 87 | } 88 | } 89 | 90 | // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 91 | fn bresenham(x int, y int, x1 int, y1 int) []Point { 92 | mut x0 := x 93 | mut y0 := y 94 | 95 | dx := abs(x1 - x0) 96 | dy := abs(y1 - y0) 97 | sx := if x0 < x1 { 1 } else { -1 } 98 | sy := if y0 < y1 { 1 } else { -1 } 99 | mut err := dx - dy 100 | 101 | mut pp := []Point{} 102 | pp << Point{x1, y1} 103 | 104 | for x0 != x1 || y0 != y1 { 105 | pp << Point{x0, y0} 106 | e2 := 2 * err 107 | if e2 > -dy { 108 | err -= dy 109 | x0 += sx 110 | } 111 | if e2 < dx { 112 | err += dx 113 | y0 += sy 114 | } 115 | } 116 | return pp 117 | } 118 | -------------------------------------------------------------------------------- /untitledv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/untitledv.png -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'paint' 3 | description: 'vpaint' 4 | version: '1.0' 5 | license: 'arr' 6 | dependencies: [] 7 | } 8 | -------------------------------------------------------------------------------- /v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/vpaint/1a41f0a814dc35b110e0ea3e930c42aca038f11e/v.png --------------------------------------------------------------------------------