├── .gitignore
├── main
├── render
│ ├── my.render
│ └── ortho_fixedaspect.render_script
├── Multiviewer_icon.ico
├── Multiviewer_icon.png
├── common
│ ├── fonts
│ │ ├── OpenSans-Light.ttf
│ │ └── opensans_light.font
│ ├── textures
│ │ ├── TEX_Cross_37.png
│ │ ├── TEX_Dot_Hard.png
│ │ ├── TEX_Dot_Soft.png
│ │ ├── TEX_Square_1.png
│ │ ├── TEX_Cross_64px.png
│ │ ├── TEX_Square_64.png
│ │ ├── Triangle_64px.png
│ │ ├── TEX_Dot_Hard-ish.png
│ │ ├── highlight_box_1.png
│ │ └── TEX_Dot_Hard_with glow.png
│ ├── materials
│ │ ├── bg_gui.fp
│ │ ├── bg_gui.material
│ │ ├── ref.vp
│ │ ├── ref.fp
│ │ ├── bg_gui.vp
│ │ └── ref.material
│ └── common.atlas
├── refs
│ ├── refs.gui
│ ├── project_io.lua
│ └── refs.gui_script
├── editor
│ ├── camera.script
│ ├── camera.go
│ └── viewport.script
├── framework
│ ├── window_manager.lua
│ ├── utilities.lua
│ └── json.lua
└── main.collection
├── game.project
├── LICENSE
├── input
└── game.input_binding
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .internal/
3 | build/
4 | /.internal
5 | /build
--------------------------------------------------------------------------------
/main/render/my.render:
--------------------------------------------------------------------------------
1 | script: "/main/render/ortho_fixedaspect.render_script"
2 |
--------------------------------------------------------------------------------
/main/Multiviewer_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/Multiviewer_icon.ico
--------------------------------------------------------------------------------
/main/Multiviewer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/Multiviewer_icon.png
--------------------------------------------------------------------------------
/main/common/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/main/common/textures/TEX_Cross_37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Cross_37.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Dot_Hard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Dot_Hard.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Dot_Soft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Dot_Soft.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Square_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Square_1.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Cross_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Cross_64px.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Square_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Square_64.png
--------------------------------------------------------------------------------
/main/common/textures/Triangle_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/Triangle_64px.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Dot_Hard-ish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Dot_Hard-ish.png
--------------------------------------------------------------------------------
/main/common/textures/highlight_box_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/highlight_box_1.png
--------------------------------------------------------------------------------
/main/common/textures/TEX_Dot_Hard_with glow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rgrams/multiviewer/HEAD/main/common/textures/TEX_Dot_Hard_with glow.png
--------------------------------------------------------------------------------
/main/refs/refs.gui:
--------------------------------------------------------------------------------
1 | script: "/main/refs/refs.gui_script"
2 | background_color {
3 | x: 0.0
4 | y: 0.0
5 | z: 0.0
6 | w: 0.0
7 | }
8 | material: "/main/common/materials/ref.material"
9 | adjust_reference: ADJUST_REFERENCE_DISABLED
10 | max_nodes: 512
11 |
--------------------------------------------------------------------------------
/main/common/materials/bg_gui.fp:
--------------------------------------------------------------------------------
1 | varying mediump vec2 var_texcoord0;
2 | varying lowp vec4 var_color;
3 |
4 | uniform lowp sampler2D texture;
5 |
6 | void main()
7 | {
8 | lowp vec4 tex = texture2D(texture, var_texcoord0.xy);
9 | gl_FragColor = tex * var_color;
10 | }
11 |
--------------------------------------------------------------------------------
/main/common/materials/bg_gui.material:
--------------------------------------------------------------------------------
1 | name: "bg_gui"
2 | vertex_program: "/main/common/materials/bg_gui.vp"
3 | fragment_program: "/main/common/materials/bg_gui.fp"
4 | vertex_constants {
5 | name: "view_proj"
6 | type: CONSTANT_TYPE_VIEWPROJ
7 | value {
8 | x: 0.0
9 | y: 0.0
10 | z: 0.0
11 | w: 0.0
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/main/common/materials/ref.vp:
--------------------------------------------------------------------------------
1 | uniform mediump mat4 view_proj;
2 |
3 | // positions are in world space
4 | attribute mediump vec4 position;
5 | attribute mediump vec2 texcoord0;
6 |
7 | varying mediump vec2 var_texcoord0;
8 |
9 | void main()
10 | {
11 | gl_Position = view_proj * vec4(position.xyz, 1.0);
12 | var_texcoord0 = texcoord0;
13 | }
14 |
--------------------------------------------------------------------------------
/main/common/materials/ref.fp:
--------------------------------------------------------------------------------
1 | varying mediump vec2 var_texcoord0;
2 |
3 | uniform lowp sampler2D DIFFUSE_TEXTURE;
4 | uniform lowp vec4 tint;
5 |
6 | void main()
7 | {
8 | lowp vec4 color = texture2D(DIFFUSE_TEXTURE, var_texcoord0.xy);
9 | color.xyz *= color.w;
10 | lowp vec3 highlight = vec3(tint.x, tint.x, tint.x);
11 | color.xyz += highlight;
12 | gl_FragColor = color;
13 | }
14 |
--------------------------------------------------------------------------------
/main/editor/camera.script:
--------------------------------------------------------------------------------
1 |
2 | local winman = require "main.framework.window_manager"
3 | local SET_POSITION = hash("set position")
4 |
5 | function init(self)
6 | msg.post("#camera", "acquire_camera_focus")
7 | end
8 |
9 | function on_message(self, message_id, message, sender)
10 | if message_id == SET_POSITION then
11 | go.set_position(message.pos)
12 | winman.camPos = message.pos
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/main/common/fonts/opensans_light.font:
--------------------------------------------------------------------------------
1 | font: "/main/common/fonts/OpenSans-Light.ttf"
2 | material: "/builtins/fonts/font.material"
3 | size: 24
4 | antialias: 1
5 | alpha: 1.0
6 | outline_alpha: 0.0
7 | outline_width: 0.0
8 | shadow_alpha: 0.0
9 | shadow_blur: 0
10 | shadow_x: 0.0
11 | shadow_y: 0.0
12 | extra_characters: ""
13 | output_format: TYPE_BITMAP
14 | all_chars: false
15 | cache_width: 0
16 | cache_height: 0
17 |
--------------------------------------------------------------------------------
/main/common/materials/bg_gui.vp:
--------------------------------------------------------------------------------
1 | uniform mediump mat4 view_proj;
2 |
3 | // positions are in world space
4 | attribute mediump vec4 position;
5 | attribute mediump vec2 texcoord0;
6 | attribute lowp vec4 color;
7 |
8 | varying mediump vec2 var_texcoord0;
9 | varying lowp vec4 var_color;
10 |
11 | void main()
12 | {
13 | var_texcoord0 = texcoord0;
14 | var_color = color;
15 | gl_Position = view_proj * vec4(position.xyz, 1.0);
16 | }
17 |
--------------------------------------------------------------------------------
/main/common/materials/ref.material:
--------------------------------------------------------------------------------
1 | name: "sprite"
2 | tags: "tile"
3 | vertex_program: "/main/common/materials/ref.vp"
4 | fragment_program: "/main/common/materials/ref.fp"
5 | vertex_constants {
6 | name: "view_proj"
7 | type: CONSTANT_TYPE_VIEWPROJ
8 | value {
9 | x: 0.0
10 | y: 0.0
11 | z: 0.0
12 | w: 0.0
13 | }
14 | }
15 | fragment_constants {
16 | name: "tint"
17 | type: CONSTANT_TYPE_USER
18 | value {
19 | x: 0.0
20 | y: 1.0
21 | z: 1.0
22 | w: 1.0
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/main/editor/camera.go:
--------------------------------------------------------------------------------
1 | components {
2 | id: "script"
3 | component: "/main/editor/camera.script"
4 | position {
5 | x: 0.0
6 | y: 0.0
7 | z: 0.0
8 | }
9 | rotation {
10 | x: 0.0
11 | y: 0.0
12 | z: 0.0
13 | w: 1.0
14 | }
15 | }
16 | embedded_components {
17 | id: "camera"
18 | type: "camera"
19 | data: "aspect_ratio: 1.0\n"
20 | "fov: 45.0\n"
21 | "near_z: 0.1\n"
22 | "far_z: 1000.0\n"
23 | "auto_aspect_ratio: 1\n"
24 | ""
25 | position {
26 | x: 0.0
27 | y: 0.0
28 | z: 0.0
29 | }
30 | rotation {
31 | x: 0.0
32 | y: 0.0
33 | z: 0.0
34 | w: 1.0
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/main/common/common.atlas:
--------------------------------------------------------------------------------
1 | images {
2 | image: "/main/common/textures/TEX_Cross_37.png"
3 | }
4 | images {
5 | image: "/main/common/textures/TEX_Cross_64px.png"
6 | }
7 | images {
8 | image: "/main/common/textures/TEX_Dot_Hard-ish.png"
9 | }
10 | images {
11 | image: "/main/common/textures/TEX_Dot_Hard.png"
12 | }
13 | images {
14 | image: "/main/common/textures/TEX_Dot_Hard_with glow.png"
15 | }
16 | images {
17 | image: "/main/common/textures/TEX_Dot_Soft.png"
18 | }
19 | images {
20 | image: "/main/common/textures/TEX_Square_1.png"
21 | }
22 | images {
23 | image: "/main/common/textures/TEX_Square_64.png"
24 | }
25 | images {
26 | image: "/main/common/textures/Triangle_64px.png"
27 | }
28 | images {
29 | image: "/main/common/textures/highlight_box_1.png"
30 | }
31 | margin: 0
32 | extrude_borders: 0
33 | inner_padding: 0
34 |
--------------------------------------------------------------------------------
/main/refs/project_io.lua:
--------------------------------------------------------------------------------
1 |
2 | local M = {}
3 |
4 |
5 | M.fileExt = "multiview"
6 | local data = {}
7 |
8 |
9 | function M.get_file_extension(path)
10 | local dotPos = string.find(path, "%.[^%.]+$") -- find pattern: dot, any number of non-dot characters, then end of string
11 | if dotPos then
12 | return string.sub(path, dotPos+1)
13 | else
14 | return nil
15 | end
16 | end
17 |
18 | function M.ensure_file_extension(path)
19 | local dotPos = string.find(path, "%.[^%.]+$")
20 | if dotPos and string.sub(path, dotPos+1) == M.fileExt then
21 | return path
22 | else
23 | path = path .. "." .. M.fileExt
24 | end
25 | return path
26 | end
27 |
28 | function M.load_project_file(path)
29 | local f = io.open(path, "r")
30 | local str = f:read("*a")
31 | local images = json.decode(str)
32 | pprint(images)
33 | return images
34 | end
35 |
36 |
37 | return M
38 |
--------------------------------------------------------------------------------
/game.project:
--------------------------------------------------------------------------------
1 | [project]
2 | title = MultiViewer
3 | version = 1.2.3
4 | dependencies = https://github.com/andsve/def-diags/archive/master.zip
5 |
6 | [bootstrap]
7 | main_collection = /main/main.collectionc
8 | render = /main/render/my.renderc
9 |
10 | [input]
11 | game_binding = /input/game.input_bindingc
12 |
13 | [display]
14 | width = 800
15 | height = 500
16 |
17 | [physics]
18 | scale = 0.02
19 |
20 | [script]
21 | shared_state = 1
22 |
23 | [graphics]
24 | default_texture_min_filter = nearest
25 | default_texture_mag_filter = nearest
26 |
27 | [render]
28 | clear_color_red = 0.2
29 | clear_color_green = 0.2
30 | clear_color_blue = 0.2
31 |
32 | [spine]
33 | max_count = 0
34 |
35 | [particle_fx]
36 | max_count = 0
37 | max_particle_count = 0
38 |
39 | [collectionfactory]
40 | max_count = 4
41 |
42 | [factory]
43 | max_count = 4
44 |
45 | [windows]
46 | app_icon = /main/Multiviewer_icon.ico
47 |
48 |
--------------------------------------------------------------------------------
/main/framework/window_manager.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | -- These are set from the render script. Defined here for autocomplete purposes
4 | M.worldHalfx = 400
5 | M.worldHalfy = 275
6 | M.halfx = 400
7 | M.halfy = 275
8 | M.zoom = 1
9 | M.zoom_listeners = {}
10 |
11 | M.scale = 1 -- view scale factor from render script
12 | M.barOffset = vmath.vector3() -- x, y size of black bars outside viewport
13 |
14 | M.camPos = vmath.vector3()
15 | M.playerPos = vmath.vector3()
16 |
17 | M.render_update = nil
18 |
19 | function M.mouse_to_world(mx, my) -- Uses screen_x, screen_y
20 | return (mx - M.halfx)/M.scale, (my - M.halfy)/M.scale
21 | end
22 |
23 | function M.set_cam_pos(pos)
24 | M.camPos = pos
25 | go.set_position(M.camPos, "camera")
26 | end
27 |
28 | function M.set_zoom(newZoom)
29 | M.zoom = newZoom
30 | for i, v in ipairs(M.zoom_listeners) do
31 | msg.post(v, "zoom", {zoom = M.zoom})
32 | end
33 | end
34 |
35 | return M
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 rgrams
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 |
--------------------------------------------------------------------------------
/input/game.input_binding:
--------------------------------------------------------------------------------
1 | key_trigger {
2 | input: KEY_ESC
3 | action: "escape"
4 | }
5 | key_trigger {
6 | input: KEY_SPACE
7 | action: "space"
8 | }
9 | key_trigger {
10 | input: KEY_ENTER
11 | action: "enter"
12 | }
13 | key_trigger {
14 | input: KEY_DEL
15 | action: "delete"
16 | }
17 | key_trigger {
18 | input: KEY_PAGEUP
19 | action: "move up"
20 | }
21 | key_trigger {
22 | input: KEY_PAGEDOWN
23 | action: "move down"
24 | }
25 | key_trigger {
26 | input: KEY_LCTRL
27 | action: "ctrl"
28 | }
29 | key_trigger {
30 | input: KEY_RCTRL
31 | action: "ctrl"
32 | }
33 | key_trigger {
34 | input: KEY_O
35 | action: "o"
36 | }
37 | key_trigger {
38 | input: KEY_S
39 | action: "s"
40 | }
41 | key_trigger {
42 | input: KEY_LSHIFT
43 | action: "shift"
44 | }
45 | key_trigger {
46 | input: KEY_RSHIFT
47 | action: "shift"
48 | }
49 | mouse_trigger {
50 | input: MOUSE_BUTTON_LEFT
51 | action: "mouse click"
52 | }
53 | mouse_trigger {
54 | input: MOUSE_WHEEL_UP
55 | action: "zoom in"
56 | }
57 | mouse_trigger {
58 | input: MOUSE_WHEEL_DOWN
59 | action: "zoom out"
60 | }
61 | mouse_trigger {
62 | input: MOUSE_BUTTON_MIDDLE
63 | action: "pan"
64 | }
65 | mouse_trigger {
66 | input: MOUSE_BUTTON_RIGHT
67 | action: "scale"
68 | }
69 |
--------------------------------------------------------------------------------
/main/main.collection:
--------------------------------------------------------------------------------
1 | name: "main"
2 | instances {
3 | id: "camera"
4 | prototype: "/main/editor/camera.go"
5 | position {
6 | x: 0.0
7 | y: 0.0
8 | z: 0.0
9 | }
10 | rotation {
11 | x: 0.0
12 | y: 0.0
13 | z: 0.0
14 | w: 1.0
15 | }
16 | scale3 {
17 | x: 1.0
18 | y: 1.0
19 | z: 1.0
20 | }
21 | }
22 | scale_along_z: 0
23 | embedded_instances {
24 | id: "refs"
25 | data: "components {\n"
26 | " id: \"refs\"\n"
27 | " component: \"/main/refs/refs.gui\"\n"
28 | " position {\n"
29 | " x: 0.0\n"
30 | " y: 0.0\n"
31 | " z: 0.0\n"
32 | " }\n"
33 | " rotation {\n"
34 | " x: 0.0\n"
35 | " y: 0.0\n"
36 | " z: 0.0\n"
37 | " w: 1.0\n"
38 | " }\n"
39 | "}\n"
40 | ""
41 | position {
42 | x: 0.0
43 | y: 0.0
44 | z: 0.0
45 | }
46 | rotation {
47 | x: 0.0
48 | y: 0.0
49 | z: 0.0
50 | w: 1.0
51 | }
52 | scale3 {
53 | x: 1.0
54 | y: 1.0
55 | z: 1.0
56 | }
57 | }
58 | embedded_instances {
59 | id: "editor"
60 | data: "components {\n"
61 | " id: \"viewport\"\n"
62 | " component: \"/main/editor/viewport.script\"\n"
63 | " position {\n"
64 | " x: 0.0\n"
65 | " y: 0.0\n"
66 | " z: 0.0\n"
67 | " }\n"
68 | " rotation {\n"
69 | " x: 0.0\n"
70 | " y: 0.0\n"
71 | " z: 0.0\n"
72 | " w: 1.0\n"
73 | " }\n"
74 | "}\n"
75 | ""
76 | position {
77 | x: 0.0
78 | y: 0.0
79 | z: 0.0
80 | }
81 | rotation {
82 | x: 0.0
83 | y: 0.0
84 | z: 0.0
85 | w: 1.0
86 | }
87 | scale3 {
88 | x: 1.0
89 | y: 1.0
90 | z: 1.0
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/main/editor/viewport.script:
--------------------------------------------------------------------------------
1 |
2 | local winman = require "main.framework.window_manager"
3 |
4 | local zoom = 0.9
5 | local bgSpriteSize = 64
6 |
7 |
8 | local function update_mouse(self)
9 | self.mousePos.x, self.mousePos.y = winman.mouse_to_world(self.mouse_spos.x, self.mouse_spos.y)
10 | self.mousePos = self.mousePos * winman.zoom + winman.camPos
11 | msg.post("refs#refs", "update mouse", {pos = self.mousePos, move = self.moving, scale = self.scaling})
12 | end
13 |
14 | function init(self)
15 | msg.post(".", "acquire_input_focus")
16 | self.url = msg.url()
17 | self.panvect = vmath.vector3()
18 | self.mousePos = vmath.vector3()
19 | self.mouse_spos = vmath.vector3()
20 | self.moving = false
21 | self.scaling = false
22 | table.insert(winman.zoom_listeners, self.url)
23 | end
24 |
25 | function on_input(self, action_id, action)
26 | if not action_id then
27 | if self.panning then
28 | self.panvect.x = action.screen_dx * winman.zoom
29 | self.panvect.y = action.screen_dy * winman.zoom
30 | winman.set_cam_pos(winman.camPos - self.panvect)
31 | end
32 | self.mouse_spos.x = action.screen_x
33 | self.mouse_spos.y = action.screen_y
34 | update_mouse(self)
35 | elseif action.pressed then
36 | if action_id == hash("zoom in") then
37 | msg.post("@render:", "zoom", {zoom = zoom})
38 | elseif action_id == hash("zoom out") then
39 | msg.post("@render:", "zoom", {zoom = 1/zoom})
40 | elseif action_id == hash("pan") then
41 | self.panning = true
42 | elseif action_id == hash("escape") then
43 | msg.post("@system:", "exit", {code = 0})
44 | elseif action_id == hash("mouse click") then
45 | self.moving = true
46 | elseif action_id == hash("scale") then
47 | self.scaling = true
48 | end
49 | elseif action.released then
50 | if action_id == hash("pan") then
51 | self.panning = false
52 | elseif action_id == hash("mouse click") then
53 | self.moving = false
54 | elseif action_id == hash("scale") then
55 | self.scaling = false
56 | end
57 | end
58 | end
59 |
60 | function on_message(self, message_id, message)
61 | if message_id == hash("zoom") then
62 | update_mouse(self)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # MultiViewer
3 | _A very basic multi-image viewer intended for displaying art references. Made with Defold._
4 |
5 | The two popular art reference viewers out there, Kuadro and PureRef, are cool, but I don't like them for various reasons. Kuadro, last I tried it, was a pain because every image was a separate window. PureRef doesn't have that issue, but it has very weird controls and hotkeys, and doesn't let me use the Windows hotkeys for moving the window around, maximizing it, etc.
6 |
7 | My MultiViewer is not meant to compete with those programs. It has only the most basic features necessary for it to be useful: moving and scaling images and the viewport and saving and loading "projects"(groups of images). It doesn't support a lot of image types (progressive jpegs for example), only PNGs, most JPEGs, and GIFs (it won't animate them). I generally use it on my second monitor. Since it can't be "always on top" of other windows (without external software), it's probably not too useful if you only have one monitor like a normal person.
8 |
9 | [Defold forum thread here.](https://forum.defold.com/t/multi-image-viewer-tool/11991)
10 |
11 | [Demo .gif](https://forum.defold.com/uploads/default/original/2X/6/65b3bb071a006da7c834f519fa02b55646d410db.gif "A short gif of usage")
12 |
13 | > NOTE: I have now rewritten this program in Love2D. Source repo here: https://github.com/rgrams/multiviewer-2
14 |
15 | ### Controls
16 | * Ctrl-O or Space -- Open images or project files (.multiview)
17 | * Ctrl-S -- Save project
18 | * Ctrl-Shift-S -- Save project as...
19 | * Left-Click-Drag -- Move image
20 | * Right-Click-Drag -- Scale image
21 | * Middle-Click-Drag -- Pan Viewport
22 | * Mouse Wheel -- Zoom Viewport
23 | * Page Up -- Move image under cursor up in draw order
24 | * Page Down -- Move image under cursor down in draw order
25 |
26 | #### Notes on Saving & Loading Projects
27 | * Opening a .multiview file just adds in the saved images to your viewport, it won't remove any images you've already loaded.
28 | * Saving a project will record all the images you have open.
29 | * You don't have to type in the file extension when saving, it will add it on automatically if it's not there.
30 | * You can open images and project files at the same time.
31 |
--------------------------------------------------------------------------------
/main/framework/utilities.lua:
--------------------------------------------------------------------------------
1 | -- Version 1.2
2 |
3 | local M = {}
4 |
5 |
6 | -- Angle-Diff (gets the smallest angle between two angles, using radians)
7 | function M.anglediff_rad(rad1, rad2)
8 | local a = rad1 - rad2
9 | a = (a + math.pi) % (math.pi * 2) - math.pi
10 | return a
11 | end
12 |
13 | -- Angle-Diff (gets the smallest angle between two angles, using degrees)
14 | function M.anglediff_deg(deg1, deg2)
15 | local a = deg1 - deg2
16 | a = (a + 180) % (180 * 2) - 180
17 | return a
18 | end
19 |
20 | -- Round
21 | function M.round(x)
22 | local a = x % 1
23 | x = x - a
24 | if a < 0.5 then a = 0
25 | else a = 1 end
26 | return x + a
27 | end
28 |
29 | -- Clamp
30 | function M.clamp(x, min, max)
31 | if x > max then x = max
32 | elseif x < min then x = min
33 | end
34 | return x
35 | end
36 |
37 | --Sign
38 | function M.sign(x)
39 | if x >= 0 then return 1
40 | else return -1
41 | end
42 | end
43 |
44 | -- Find (in array table)
45 | function M.find(t, val)
46 | for i, v in ipairs(t) do
47 | if v == val then return i end
48 | end
49 | end
50 |
51 | -- Find & Remove (from array table)
52 | function M.find_remove(t, val)
53 | for i, v in ipairs(t) do
54 | if v == val then
55 | table.remove(t, i)
56 | return i
57 | end
58 | end
59 | end
60 |
61 | -- Make a shallow copy of a table
62 | function M.shallow_copy(t)
63 | local t2 = {}
64 | for k,v in pairs(t) do
65 | t2[k] = v
66 | end
67 | return t2
68 | end
69 |
70 | -- Make a deep copy of a table
71 | function M.deep_copy(t)
72 | local t2 = {}
73 | for k, v in pairs(t) do
74 | if type(v) == "table" then
75 | t2[k] = M.deep_copy(v)
76 | else
77 | t2[k] = v
78 | end
79 | end
80 | return t2
81 | end
82 |
83 | -- Swap two elements in table
84 | function M.swap_elements(t, i1, i2)
85 | t[i1], t[i2] = t[i2], t[i1]
86 | end
87 |
88 | -- Next index in array (looping)
89 | function M.nexti(t, i)
90 | if #t == 0 then return 0 end
91 | i = i + 1
92 | if i > #t then i = 1 end
93 | return i
94 | end
95 |
96 | -- Previous index in array (looping)
97 | function M.previ(t, i)
98 | i = i - 1
99 | if i < 1 then i = #t end
100 | return i
101 | end
102 |
103 | -- Next value in array (looping)
104 | function M.nextval(t, i)
105 | return t[M.nexti(t, i)]
106 | end
107 |
108 | -- Previous value in array (looping)
109 | function M.prevval(t, i)
110 | return t[M.previ(t, i)]
111 | end
112 |
113 | -- Vect to Quat
114 | function M.vect_to_quat(vect)
115 | return vmath.quat_rotation_z(math.atan2(vect.y, vect.x))
116 | end
117 |
118 | -- Vect to Quat + 90 degrees (perpendicular)
119 | function M.vect_to_quat90(vect)
120 | return vmath.quat_rotation_z(math.atan2(vect.y, vect.x) + math.pi/2)
121 | end
122 |
123 | -- Get script URL
124 | function M.scripturl(path)
125 | return msg.url(nil, path, "script")
126 | end
127 |
128 | -- Random float from -1 to 1
129 | function M.rand11()
130 | return((math.random() - 0.5) * 2)
131 | end
132 |
133 | -- Random float in range
134 | function M.rand_range(min, max)
135 | return math.random() * (max - min) + min
136 | end
137 |
138 |
139 | return M
140 |
--------------------------------------------------------------------------------
/main/render/ortho_fixedaspect.render_script:
--------------------------------------------------------------------------------
1 |
2 | local winman = require "main.framework.window_manager"
3 | local original_width
4 | local original_height
5 | local window_width
6 | local window_height
7 | local final_width
8 | local final_height
9 | local xoffset
10 | local yoffset
11 | local zoom = 3
12 |
13 |
14 | function init(self)
15 | winman.render_update = function() render_update(self) end
16 |
17 | self.tile_pred = render.predicate({"tile"})
18 | self.gui_pred = render.predicate({"gui"})
19 | self.text_pred = render.predicate({"text"})
20 |
21 | self.clear_color = vmath.vector4()
22 | self.clear_color.x = sys.get_config("render.clear_color_red", 0)
23 | self.clear_color.y = sys.get_config("render.clear_color_green", 0)
24 | self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
25 | self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)
26 |
27 | self.view = vmath.matrix4()
28 | self.projection = vmath.matrix4()
29 | original_width = 800
30 | original_height = 550
31 |
32 | update_window(self)
33 | end
34 |
35 | function update(self, dt)
36 | render.set_depth_mask(true)
37 | render.clear({[render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_STENCIL_BIT] = 0})
38 |
39 | render.set_viewport(0, 0, final_width, final_height)
40 |
41 | render.set_view(self.view)
42 |
43 | render.set_depth_mask(false)
44 | render.disable_state(render.STATE_DEPTH_TEST)
45 | render.disable_state(render.STATE_STENCIL_TEST)
46 | render.enable_state(render.STATE_BLEND)
47 | render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
48 | render.disable_state(render.STATE_CULL_FACE)
49 |
50 | local left, right = -final_width/2 * zoom, final_width/2 * zoom
51 | local bottom, top = -final_height/2 * zoom, final_height/2 * zoom
52 |
53 | render.set_projection(vmath.matrix4_orthographic(left, right, bottom, top, -1, 1))
54 |
55 | render.draw(self.tile_pred)
56 | render.draw_debug3d()
57 |
58 | render.set_view(vmath.matrix4())
59 | render.set_projection(vmath.matrix4_orthographic(xoffset, xoffset + final_width, yoffset, yoffset + final_height, -1, 1))
60 |
61 | render.enable_state(render.STATE_STENCIL_TEST)
62 | render.draw(self.gui_pred)
63 | render.draw(self.text_pred)
64 | render.disable_state(render.STATE_STENCIL_TEST)
65 | end
66 |
67 | function on_message(self, message_id, message)
68 | if message_id == hash("clear_color") then
69 | self.clear_color = message.color
70 | elseif message_id == hash("set_view_projection") then
71 | self.view = message.view
72 | elseif message_id == hash("set_zoom") then
73 | zoom = message.zoom
74 | winman.set_zoom(zoom)
75 | elseif message_id == hash("zoom") then
76 | zoom = zoom * message.zoom
77 | winman.set_zoom(zoom)
78 | elseif message_id == hash("window_resized") then
79 | update_window(self)
80 | end
81 | end
82 |
83 | function update_window(self)
84 | window_width = render.get_window_width()
85 | window_height = render.get_window_height()
86 | winman.halfx, winman.halfy = window_width / 2, window_height / 2
87 |
88 | final_width = window_width
89 | final_height = window_height
90 | xoffset = -(final_width - window_width) / 2 -- thickness of black bars
91 | yoffset = -(final_height - window_height) / 2 -- thickness of black bars
92 | winman.set_zoom(zoom)
93 | end
94 |
--------------------------------------------------------------------------------
/main/framework/json.lua:
--------------------------------------------------------------------------------
1 | --[[ json.lua
2 |
3 | A compact pure-Lua JSON library.
4 | The main functions are: json.stringify, json.parse.
5 |
6 | ## json.stringify:
7 |
8 | This expects the following to be true of any tables being encoded:
9 | * They only have string or number keys. Number keys must be represented as
10 | strings in json; this is part of the json spec.
11 | * They are not recursive. Such a structure cannot be specified in json.
12 |
13 | A Lua table is considered to be an array if and only if its set of keys is a
14 | consecutive sequence of positive integers starting at 1. Arrays are encoded like
15 | so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json
16 | object, encoded like so: `{"key1": 2, "key2": false}`.
17 |
18 | Because the Lua nil value cannot be a key, and as a table value is considerd
19 | equivalent to a missing key, there is no way to express the json "null" value in
20 | a Lua table. The only way this will output "null" is if your entire input obj is
21 | nil itself.
22 |
23 | An empty Lua table, {}, could be considered either a json object or array -
24 | it's an ambiguous edge case. We choose to treat this as an object as it is the
25 | more general type.
26 |
27 | To be clear, none of the above considerations is a limitation of this code.
28 | Rather, it is what we get when we completely observe the json specification for
29 | as arbitrary a Lua object as json is capable of expressing.
30 |
31 | ## json.parse:
32 |
33 | This function parses json, with the exception that it does not pay attention to
34 | \u-escaped unicode code points in strings.
35 |
36 | It is difficult for Lua to return null as a value. In order to prevent the loss
37 | of keys with a null value in a json string, this function uses the one-off
38 | table value json.null (which is just an empty table) to indicate null values.
39 | This way you can check if a value is null with the conditional
40 | `val == json.null`.
41 |
42 | If you have control over the data and are using Lua, I would recommend just
43 | avoiding null values in your data to begin with.
44 |
45 | --]]
46 |
47 |
48 | local json = {}
49 |
50 |
51 | -- Internal functions.
52 |
53 | local function kind_of(obj)
54 | if type(obj) ~= 'table' then return type(obj) end
55 | local i = 1
56 | for _ in pairs(obj) do
57 | if obj[i] ~= nil then i = i + 1 else return 'table' end
58 | end
59 | if i == 1 then return 'table' else return 'array' end
60 | end
61 |
62 | local function escape_str(s)
63 | local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'}
64 | local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'}
65 | for i, c in ipairs(in_char) do
66 | s = s:gsub(c, '\\' .. out_char[i])
67 | end
68 | return s
69 | end
70 |
71 | -- Returns pos, did_find; there are two cases:
72 | -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
73 | -- 2. Delimiter not found: pos = pos after leading space; did_find = false.
74 | -- This throws an error if err_if_missing is true and the delim is not found.
75 | local function skip_delim(str, pos, delim, err_if_missing)
76 | pos = pos + #str:match('^%s*', pos)
77 | if str:sub(pos, pos) ~= delim then
78 | if err_if_missing then
79 | error('Expected ' .. delim .. ' near position ' .. pos)
80 | end
81 | return pos, false
82 | end
83 | return pos + 1, true
84 | end
85 |
86 | -- Expects the given pos to be the first character after the opening quote.
87 | -- Returns val, pos; the returned pos is after the closing quote character.
88 | local function parse_str_val(str, pos, val)
89 | val = val or ''
90 | local early_end_error = 'End of input found while parsing string.'
91 | if pos > #str then error(early_end_error) end
92 | local c = str:sub(pos, pos)
93 | if c == '"' then return val, pos + 1 end
94 | if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
95 | -- We must have a \ character.
96 | local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
97 | local nextc = str:sub(pos + 1, pos + 1)
98 | if not nextc then error(early_end_error) end
99 | return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
100 | end
101 |
102 | -- Returns val, pos; the returned pos is after the number's final character.
103 | local function parse_num_val(str, pos)
104 | local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
105 | local val = tonumber(num_str)
106 | if not val then error('Error parsing number at position ' .. pos .. '.') end
107 | return val, pos + #num_str
108 | end
109 |
110 |
111 | -- Public values and functions.
112 |
113 | function json.stringify(obj, as_key)
114 | local s = {} -- We'll build the string as an array of strings to be concatenated.
115 | local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise.
116 | if kind == 'array' then
117 | if as_key then error('Can\'t encode array as key.') end
118 | s[#s + 1] = '['
119 | for i, val in ipairs(obj) do
120 | if i > 1 then s[#s + 1] = ', ' end
121 | s[#s + 1] = json.stringify(val)
122 | end
123 | s[#s + 1] = ']'
124 | elseif kind == 'table' then
125 | if as_key then error('Can\'t encode table as key.') end
126 | s[#s + 1] = '{'
127 | for k, v in pairs(obj) do
128 | if #s > 1 then s[#s + 1] = ', ' end
129 | s[#s + 1] = json.stringify(k, true)
130 | s[#s + 1] = ':'
131 | s[#s + 1] = json.stringify(v)
132 | end
133 | s[#s + 1] = '}'
134 | elseif kind == 'string' then
135 | return '"' .. escape_str(obj) .. '"'
136 | elseif kind == 'number' then
137 | if as_key then return '"' .. tostring(obj) .. '"' end
138 | return tostring(obj)
139 | elseif kind == 'boolean' then
140 | return tostring(obj)
141 | elseif kind == 'nil' then
142 | return 'null'
143 | else
144 | error('Unjsonifiable type: ' .. kind .. '.')
145 | end
146 | return table.concat(s)
147 | end
148 |
149 | json.null = {} -- This is a one-off table to represent the null value.
150 |
151 | function json.parse(str, pos, end_delim)
152 | pos = pos or 1
153 | if pos > #str then error('Reached unexpected end of input.') end
154 | local pos = pos + #str:match('^%s*', pos) -- Skip whitespace.
155 | local first = str:sub(pos, pos)
156 | if first == '{' then -- Parse an object.
157 | local obj, key, delim_found = {}, true, true
158 | pos = pos + 1
159 | while true do
160 | key, pos = json.parse(str, pos, '}')
161 | if key == nil then return obj, pos end
162 | if not delim_found then error('Comma missing between object items.') end
163 | pos = skip_delim(str, pos, ':', true) -- true -> error if missing.
164 | obj[key], pos = json.parse(str, pos)
165 | pos, delim_found = skip_delim(str, pos, ',')
166 | end
167 | elseif first == '[' then -- Parse an array.
168 | local arr, val, delim_found = {}, true, true
169 | pos = pos + 1
170 | while true do
171 | val, pos = json.parse(str, pos, ']')
172 | if val == nil then return arr, pos end
173 | if not delim_found then error('Comma missing between array items.') end
174 | arr[#arr + 1] = val
175 | pos, delim_found = skip_delim(str, pos, ',')
176 | end
177 | elseif first == '"' then -- Parse a string.
178 | return parse_str_val(str, pos + 1)
179 | elseif first == '-' or first:match('%d') then -- Parse a number.
180 | return parse_num_val(str, pos)
181 | elseif first == end_delim then -- End of an object or array.
182 | return nil, pos + 1
183 | else -- Parse true, false, or null.
184 | local literals = {['true'] = true, ['false'] = false, ['null'] = json.null}
185 | for lit_str, lit_val in pairs(literals) do
186 | local lit_end = pos + #lit_str - 1
187 | if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
188 | end
189 | local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
190 | error('Invalid json syntax starting at ' .. pos_info_str)
191 | end
192 | end
193 |
194 | return json
195 |
--------------------------------------------------------------------------------
/main/refs/refs.gui_script:
--------------------------------------------------------------------------------
1 |
2 | local winman = require "main.framework.window_manager"
3 | local util = require "main.framework.utilities"
4 | local proj_io = require "main.refs.project_io"
5 | local json_encoder = require "main.framework.json"
6 |
7 | local inputPath = "2.png"
8 | local scaleLineColor = vmath.vector4(1, 1, 1, 1)
9 |
10 |
11 | local function load_and_set_png(self, path, node)
12 | local success = true
13 |
14 | local t = socket.gettime()
15 | local f = io.open(path, "rb")
16 | local bytes = f:read("*a")
17 | local png = image.load(bytes)
18 | if png then
19 | local channels = #png.buffer / (png.width * png.height)
20 | gui.new_texture(path, png.width, png.height, string.sub("rgba", 1, channels), png.buffer)
21 | gui.set_texture(node, path)
22 | gui.set_size(node, vmath.vector3(png.width, png.height, 0))
23 | print("Loading image number " .. #self.imageNodes .. " took " .. (socket.gettime() - t) * 1000 .. "ms")
24 | else
25 | print("ERROR: Failed to load: " .. path)
26 | success = false
27 | end
28 | return success
29 | end
30 |
31 | local function new_node(pos, size)
32 | pos = pos or winman.camPos
33 | size = size or vmath.vector3(150, 100, 0)
34 | local n = gui.new_box_node(pos, size)
35 | return n
36 | end
37 |
38 | function init(self)
39 | self.images = {}
40 | self.imageNodes = {}
41 | self.hoverNode = nil --{ n = false, i = 0 }
42 | self.mousePos = vmath.vector3()
43 | self.ctrl_pressed = false
44 | msg.post(".", "acquire_input_focus")
45 | end
46 |
47 | local function move_z(self, index, dir)
48 | local i2 = index + dir
49 | if self.images[i2] then
50 | if dir == 1 then
51 | gui.move_above(self.images[index].node, self.images[i2].node)
52 | else
53 | gui.move_below(self.images[index].node, self.images[i2].node)
54 | end
55 | self.images[index].z, self.images[i2].z = self.images[i2].z, self.images[index].z
56 | util.swap_elements(self.images, index, i2)
57 | self.hoverNode.z = i2
58 | end
59 | end
60 |
61 | local function create_ref(self, path, pos, size)
62 | local n = new_node(pos)
63 | if load_and_set_png(self, path, n) then
64 | if pos then gui.set_position(n, pos)
65 | else pos = gui.get_position(n)
66 | end
67 | if size then gui.set_size(n, size)
68 | else size = gui.get_size(n)
69 | end
70 | local dat = {
71 | node = n,
72 | path = path,
73 | z = #self.images + 1,
74 | pos = { x = pos.x, y = pos.y },
75 | size = { x = size.x, y = size.y }
76 | }
77 | table.insert(self.images, dat)
78 | else
79 | print("image load failed, deleting GUI node")
80 | gui.delete_node(n)
81 | end
82 | end
83 |
84 | local function open(self)
85 | local code, files = diags.open_multiple()
86 | print("Open multiple files - code = ", code)
87 | pprint(files)
88 | if code == 1 then
89 | for i, path in ipairs(files) do
90 | local ext = proj_io.get_file_extension(path)
91 | if ext == proj_io.fileExt then -- Open project file.
92 | self.projectPath = path
93 | local fileContents = proj_io.load_project_file(path)
94 | local images = fileContents.images
95 | if fileContents[i] then images = fileContents end -- Old format, images list only.
96 | for i, v in ipairs(images) do
97 | local pos = vmath.vector3(v.pos.x, v.pos.y, 0)
98 | local size = vmath.vector3(v.size.x, v.size.y, 0)
99 | create_ref(self, v.path, pos, size)
100 | end
101 | if fileContents.camera then
102 | local camData = fileContents.camera
103 | local camPos = vmath.vector3(camData.pos.x, camData.pos.y, 0)
104 | msg.post("/camera#script", "set position", {pos = camPos}) -- Can't use go. functions to set it directly.
105 | msg.post("@render:", "set_zoom", {zoom = camData.zoom})
106 | end
107 | else -- Open image file.
108 | create_ref(self, path)
109 | end
110 | end
111 | end
112 | end
113 |
114 | local function save_project(self, path)
115 | print("Saving project...")
116 | path = proj_io.ensure_file_extension(path)
117 | local f = io.open(path, "w+")
118 | local imageData = util.deep_copy(self.images)
119 | for i, v in ipairs(imageData) do
120 | v.node = nil
121 | end
122 | local camData = {
123 | pos = { x = winman.camPos.x, y = winman.camPos.y },
124 | zoom = winman.zoom,
125 | }
126 | local saveData = {
127 | images = imageData,
128 | camera = camData,
129 | }
130 | local jsonStr = json_encoder.stringify(saveData)
131 | f:write(jsonStr)
132 | f:close()
133 | end
134 |
135 | function on_input(self, action_id, action)
136 | if action.pressed then
137 | if action_id == hash("space") then
138 | open(self)
139 | elseif action_id == hash("ctrl") then
140 | self.ctrl_pressed = true
141 | elseif action_id == hash("shift") then
142 | self.shift_pressed = true
143 | elseif action_id == hash("o") then
144 | if self.ctrl_pressed then
145 | open(self)
146 | end
147 | elseif action_id == hash("s") then
148 | if self.ctrl_pressed then
149 | print("Save")
150 | if #self.images > 0 then
151 | if self.projectPath and not self.shift_pressed then
152 | print("\tSaving current project: ", self.projectPath)
153 | save_project(self, self.projectPath)
154 | else
155 | local code, filePath = diags.save(proj_io.fileExt)
156 | print("\tSaving as... ", code, filePath)
157 | if code == 1 then
158 | save_project(self, filePath)
159 | end
160 | end
161 | else
162 | print("Can't save project, no images loaded")
163 | end
164 | end
165 | elseif action_id == hash("delete") then
166 | if self.hoverNode then
167 | util.find_remove(self.images, self.hoverNode)
168 | gui.delete_node(self.hoverNode.node)
169 | self.hoverNode = false
170 | end
171 | elseif action_id == hash("move up") then
172 | if self.hoverNode then
173 | move_z(self, self.hoverNode.z, 1)
174 | end
175 | elseif action_id == hash("move down") then
176 | if self.hoverNode then
177 | move_z(self, self.hoverNode.z, -1)
178 | end
179 | end
180 | elseif action.released then
181 | if action_id == hash("ctrl") then
182 | self.ctrl_pressed = false
183 | elseif action_id == hash("shift") then
184 | self.shift_pressed = false
185 | end
186 | end
187 | end
188 |
189 | local function hit_check_node(node, x, y)
190 | local hit = false
191 | local pos = gui.get_position(node)
192 | local size = gui.get_size(node)
193 | local tl = (pos + size*0.5)
194 | local br = (pos - size*0.5)
195 | if x <= tl.x and x >= br.x and y <= tl.y and y >= br.y then
196 | hit = true
197 | end
198 | return hit
199 | end
200 |
201 | function on_message(self, message_id, message, sender)
202 | if message_id == hash("update mouse") then
203 | if self.hoverNode then
204 | local delta = vmath.vector3(message.pos.x - self.mousePos.x, message.pos.y - self.mousePos.y, 0)
205 | local pos = gui.get_position(self.hoverNode.node)
206 | local size = gui.get_size(self.hoverNode.node)
207 |
208 | if message.move then
209 | local newpos = pos + delta
210 | gui.set_position(self.hoverNode.node, newpos)
211 | self.hoverNode.pos.x = newpos.x; self.hoverNode.pos.y = newpos.y
212 |
213 | end
214 | if message.scale then
215 | local amount = vmath.dot(vmath.normalize(message.pos - pos), delta) * 2
216 | msg.post("@render:", "draw_line", { start_point = pos , end_point = message.pos, color = scaleLineColor })
217 | local aspect = size.x / size.y
218 | local ds = vmath.vector3(delta)
219 | local avg = (ds.x + ds.y)
220 | size.x = size.x + amount
221 | size.y = size.x / aspect
222 | gui.set_size(self.hoverNode.node, size)
223 | self.hoverNode.size.x = size.x; self.hoverNode.size.y = size.y
224 | end
225 | end
226 | if not message.move and not message.scale then
227 | self.hoverNode = false
228 | for i, v in ipairs(self.images) do
229 | gui.pick_node(v.node, message.pos.x, message.pos.y)
230 | local hit = hit_check_node(v.node, message.pos.x, message.pos.y)
231 | if hit then
232 | self.hoverNode = v
233 | end
234 | end
235 | end
236 |
237 | self.mousePos.x = message.pos.x
238 | self.mousePos.y = message.pos.y
239 | end
240 | end
241 |
--------------------------------------------------------------------------------