diff --git a/css/panels.css b/css/panels.css index ec89ba5c..c7fedabf 100644 --- a/css/panels.css +++ b/css/panels.css @@ -2062,7 +2062,8 @@ span.controller_state_section_info { pointer-events: none; } - body[mode=paint] #uv_frame { + body[mode=paint] #uv_frame, + body[mode=paint] #uv_viewport.tiled_mode { cursor: crosshair; } #uv_frame > #texture_canvas_wrapper > canvas, @@ -2076,6 +2077,16 @@ span.controller_state_section_info { object-fit: cover; object-position: 0 0; } + #uv_frame > #texture_canvas_wrapper > canvas.overlay_canvas[overlay_mode=tiled] { + width: 300%; + height: 300%; + margin-left: -100%; + margin-top: -100%; + } + #uv_frame > #texture_canvas_wrapper > canvas.overlay_canvas[overlay_mode=onion_skin] { + width: 100%; + height: 100%; + } /* Fix in Firefox + iPadOS */ #uv_frame_spacer { width: 1px; @@ -2092,6 +2103,8 @@ span.controller_state_section_info { left: 0; object-fit: cover; object-position: 0 0; + margin: -1px; + border: 1px solid var(--color-grid); } #uv_texture_grid path { fill: none; diff --git a/js/interface/actions.js b/js/interface/actions.js index 7218e0b1..e7c97fec 100644 --- a/js/interface/actions.js +++ b/js/interface/actions.js @@ -2262,6 +2262,8 @@ const BARS = { 'color_erase_mode', 'lock_alpha', 'painting_grid', + 'image_tiled_view', + 'image_onion_skin_view', ] }) Toolbars.vertex_snap = new Toolbar({ diff --git a/js/texturing/painter.js b/js/texturing/painter.js index d7ff2c9b..32953f08 100644 --- a/js/texturing/painter.js +++ b/js/texturing/painter.js @@ -249,7 +249,16 @@ const Painter = { } else { texture.edit(canvas => { - Painter.drawBrushLine(texture, x, y, event, new_face, uv) + let is_line = true; + if (BarItems.image_tiled_view.value == true && (Math.abs(Painter.current.x - x) > texture.width/2 || Math.abs(Painter.current.y - y) > texture.display_height/2)) { + is_line = false; + } + if (is_line) { + Painter.drawBrushLine(texture, x, y, event, new_face, uv); + } else { + Painter.current.x = Painter.current.y = 0 + Painter.useBrushlike(texture, x, y, event, uv) + } }, {no_undo: true, use_cache: true}); } Painter.current.x = x; @@ -3050,12 +3059,37 @@ BARS.defineActions(function() { }) new Toggle('painting_grid', { - icon: 'grid_on', + icon: 'grid_3x3', category: 'view', condition: {modes: ['paint']}, keybind: new Keybind({key: 'g'}), linked_setting: 'painting_grid' }) + new Toggle('image_tiled_view', { + category: 'paint', + icon: 'grid_view', + onChange(value) { + if (value && BarItems.image_onion_skin_view.value) { + BarItems.image_onion_skin_view.set(false); + } + UVEditor.vue.overlay_canvas_mode = value ? 'tiled' : null; + UVEditor.vue.updateTexture(); + UVEditor.updateOverlayCanvas(); + } + }) + new Toggle('image_onion_skin_view', { + category: 'paint', + icon: 'animation', + condition: () => Panels.textures.vue.maxFrameCount(), + onChange(value) { + if (value && BarItems.image_tiled_view.value) { + BarItems.image_tiled_view.set(false); + } + UVEditor.vue.overlay_canvas_mode = value ? 'onion_skin' : null; + UVEditor.vue.updateTexture(); + UVEditor.updateOverlayCanvas(); + } + }) new NumSlider('slider_brush_size', { condition: () => (Toolbox && ((Toolbox.selected.brush?.size == true) || ['draw_shape_tool'].includes(Toolbox.selected.id))), diff --git a/js/texturing/texture_flipbook.js b/js/texturing/texture_flipbook.js index 4d84dae3..077297ea 100644 --- a/js/texturing/texture_flipbook.js +++ b/js/texturing/texture_flipbook.js @@ -127,6 +127,7 @@ BARS.defineActions(function() { change: function(modify) { let slider_tex = getSliderTexture() if (!slider_tex) return; + UVEditor.previous_animation_frame = slider_tex.currentFrame; slider_tex.currentFrame = (modify(slider_tex.currentFrame + slider_tex.frameCount) % slider_tex.frameCount) || 0; let textures = Texture.all.filter(tex => tex.frameCount > 1); diff --git a/js/texturing/textures.js b/js/texturing/textures.js index edb3ab7a..6e10e5bd 100644 --- a/js/texturing/textures.js +++ b/js/texturing/textures.js @@ -206,6 +206,7 @@ class Texture { scope.canvas.width = scope.width; scope.canvas.height = scope.height; scope.ctx.drawImage(img, 0, 0); + if (UVEditor.vue.texture == this) UVEditor.updateOverlayCanvas(); } if (this.flags.has('update_uv_size_from_resolution')) { @@ -1745,6 +1746,7 @@ class Texture { this.source = this.canvas.toDataURL('image/png', 1); this.updateImageFromCanvas(); } + if (UVEditor.vue.texture == this) UVEditor.updateOverlayCanvas(); } updateChangesAfterEdit() { if (this.layers_enabled) { @@ -2645,12 +2647,15 @@ Interface.definePanels(function() { convertTouchEvent(e2); let pos = e2.clientX - timeline_offset; + let previous_frame = scope.currentFrame; scope.currentFrame = Math.clamp(Math.round((pos / timeline_width) * maxFrameCount), 0, maxFrameCount-1); + if (previous_frame == scope.currentFrame) return; let textures = Texture.all.filter(tex => tex.frameCount > 1); Texture.all.forEach(tex => { tex.currentFrame = (scope.currentFrame % tex.frameCount) || 0; }) + UVEditor.previous_animation_frame = previous_frame; TextureAnimator.update(textures); } function off(e3) { diff --git a/js/texturing/uv.js b/js/texturing/uv.js index 31b7d761..87524b4d 100644 --- a/js/texturing/uv.js +++ b/js/texturing/uv.js @@ -7,6 +7,12 @@ const UVEditor = { panel: null, sliders: {}, selected_element_faces: {}, + previous_animation_frame: 1, + overlay_canvas: (() => { + let canvas = document.createElement('canvas'); + canvas.classList.add('overlay_canvas'); + return canvas; + })(), get vue() { return this.panel.inside_vue; @@ -60,6 +66,10 @@ const UVEditor = { if (tex) { if (tex.frameCount) result.y += (tex.height / tex.frameCount) * tex.currentFrame; if (!tex.frameCount && tex.ratio != tex.getUVWidth() / tex.getUVHeight()) result.y /= tex.ratio; + if (BarItems.image_tiled_view.value == true) { + result.x = (tex.width + result.x) % tex.width; + result.y = (tex.display_height + result.y) % tex.display_height; + } } return result; }, @@ -248,6 +258,30 @@ const UVEditor = { scrollTop: focus[1] + margin[1] - UVEditor.height / 2, }, 100) }, + + updateOverlayCanvas() { + if (!Texture.selected) return; + let canvas = UVEditor.overlay_canvas; + let texture = Texture.selected; + let ctx = canvas.getContext('2d'); + if (BarItems.image_tiled_view.value == true) { + canvas.setAttribute('overlay_mode', 'tiled'); + canvas.width = texture.width * 3; + canvas.height = texture.display_height * 3; + for (let x = 0; x < 3; x++) { + for (let y = 0; y < 3; y++) { + ctx.drawImage(texture.canvas, x * texture.width, y * texture.display_height); + } + } + } else if (BarItems.image_onion_skin_view.value == true) { + canvas.setAttribute('overlay_mode', 'onion_skin'); + canvas.width = texture.width; + canvas.height = texture.display_height; + ctx.filter = `opacity(${45}%)`; + let frame = Math.clamp(UVEditor.previous_animation_frame, 0, texture.frameCount-1); + ctx.drawImage(texture.canvas, 0, frame * -texture.display_height); + } + }, //Get get width() { return this.vue.width; @@ -2105,6 +2139,7 @@ Interface.definePanels(function() { copy_brush_source: null, helper_lines: {x: -1, y: -1}, brush_type: BarItems.brush_shape.value, + overlay_canvas_mode: null, selection_rect: { pos_x: 0, pos_y: 0, @@ -2316,12 +2351,19 @@ Interface.definePanels(function() { this.texture.canvas.style.objectFit = this.texture.frameCount > 1 ? 'cover' : 'fill'; this.texture.canvas.style.imageRendering = this.texture.width < this.inner_width ? 'inherit' : 'auto'; + UVEditor.updateOverlayCanvas(); + Vue.nextTick(() => { let wrapper = this.$refs.texture_canvas_wrapper; - if (!wrapper || wrapper.firstChild == this.texture.canvas) return; - if (wrapper.firstChild) { + let overlay_canvas_mode = this.overlay_canvas_mode; + if (this.mode != 'paint') overlay_canvas_mode = null; + if (!wrapper || (wrapper.firstChild == this.texture.canvas && !overlay_canvas_mode)) return; + while (wrapper.firstChild) { wrapper.firstChild.remove(); } + if (UVEditor.overlay_canvas && overlay_canvas_mode) { + wrapper.append(UVEditor.overlay_canvas); + } wrapper.append(this.texture.canvas); }) }, @@ -3941,6 +3983,7 @@ Interface.definePanels(function() { @mouseenter="onMouseEnter($event)" @mouseleave="onMouseLeave($event)" class="checkerboard_target" + :class="{tiled_mode: overlay_canvas_mode == 'tiled'}" ref="viewport" v-if="!hidden && mode !== 'face_properties'" :style="{width: (width+8) + 'px', height: (height+8) + 'px', overflowX: (zoom > 1) ? 'scroll' : 'hidden', overflowY: (inner_height > height) ? 'scroll' : 'hidden'}" diff --git a/lang/en.json b/lang/en.json index ecfcc121..94eef4cc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1169,6 +1169,10 @@ "action.mirror_painting.description": "Mirror your paint strokes to the other side of the model", "action.lock_alpha": "Lock Alpha Channel", "action.lock_alpha.description": "Lock the transparency of all pixels", + "action.image_tiled_view": "Tiled View", + "action.image_tiled_view.description": "Enable the tiled view in the 2D editor", + "action.image_onion_skin_view": "Image Editor Onion Skin", + "action.image_onion_skin_view.description": "Enable the onion skin view in the 2D editor", "action.slider_brush_size": "Size", "action.slider_brush_size.desc": "Radius of the brush in pixels", "action.slider_brush_opacity": "Opacity",