├── .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  
2 | ## About
3 | Image Viewer & Editor written in the [V](https://vlang.io) Programming Language.
4 |
5 | 
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 | [](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 |
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 |
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
--------------------------------------------------------------------------------