├── .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 | --------------------------------------------------------------------------------