mirror of
https://github.com/JannisX11/blockbench.git
synced 2025-02-23 16:31:20 +08:00
450 lines
12 KiB
JavaScript
450 lines
12 KiB
JavaScript
class TextureLayer {
|
|
constructor(data, texture = Texture.selected, uuid) {
|
|
this.uuid = (uuid && isUUID(uuid)) ? uuid : guid();
|
|
this.texture = texture;
|
|
this.canvas = document.createElement('canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.in_limbo = false;
|
|
|
|
this.img = new Image();
|
|
this.img.onload = () => {
|
|
this.canvas.width = this.img.naturalWidth;
|
|
this.canvas.height = this.img.naturalHeight;
|
|
this.ctx.drawImage(this.img, 0, 0);
|
|
}
|
|
|
|
for (var key in TextureLayer.properties) {
|
|
TextureLayer.properties[key].reset(this);
|
|
}
|
|
|
|
if (data) this.extend(data);
|
|
}
|
|
get width() {
|
|
return this.canvas.width;
|
|
}
|
|
get height() {
|
|
return this.canvas.width;
|
|
}
|
|
get size() {
|
|
return [this.canvas.width, this.canvas.height];
|
|
}
|
|
get selected() {
|
|
return this.texture.selected_layer == this;
|
|
}
|
|
extend(data) {
|
|
for (var key in TextureLayer.properties) {
|
|
TextureLayer.properties[key].merge(this, data)
|
|
}
|
|
if (data.image_data) {
|
|
this.canvas.width = data.width || 16;
|
|
this.canvas.height = data.height || 16;
|
|
this.ctx.putImageData(data.image_data, 0, 0);
|
|
|
|
} else if (data.data_url) {
|
|
this.canvas.width = data.width || 16;
|
|
this.canvas.height = data.height || 16;
|
|
this.img.src = data.data_url;
|
|
}
|
|
}
|
|
select() {
|
|
this.texture.selected_layer = this;
|
|
BarItems.layer_opacity.update();
|
|
}
|
|
showContextMenu(event) {
|
|
if (!this.selected) this.select();
|
|
this.menu.open(event, this);
|
|
}
|
|
remove(undo) {
|
|
if (undo) {
|
|
Undo.initEdit({textures: [this.texture], bitmap: true});
|
|
}
|
|
let index = this.texture.layers.indexOf(this);
|
|
this.texture.layers.splice(index, 1);
|
|
if (this.texture.selected_layer == this) this.texture.selected_layer = this.texture.layers[index-1] || this.texture.layers[index];
|
|
if (undo) {
|
|
this.texture.updateLayerChanges(true);
|
|
Undo.finishEdit('Remove layer');
|
|
}
|
|
}
|
|
getUndoCopy(image_data) {
|
|
let copy = {};
|
|
copy.texture = this.texture.uuid;
|
|
for (var key in TextureLayer.properties) {
|
|
TextureLayer.properties[key].copy(this, copy);
|
|
}
|
|
copy.width = this.width;
|
|
copy.height = this.height;
|
|
if (image_data) {
|
|
copy.image_data = this.ctx.getImageData(0, 0, this.width, this.height);
|
|
}
|
|
return copy;
|
|
}
|
|
getSaveCopy() {
|
|
let copy = {};
|
|
for (var key in TextureLayer.properties) {
|
|
TextureLayer.properties[key].copy(this, copy);
|
|
}
|
|
copy.width = this.width;
|
|
copy.height = this.height;
|
|
copy.data_url = this.canvas.toDataURL();
|
|
return copy;
|
|
}
|
|
setLimbo() {
|
|
this.in_limbo = true;
|
|
}
|
|
setSize(width, height) {
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
}
|
|
toggleVisibility() {
|
|
Undo.initEdit({textures: [this.texture]});
|
|
this.visible = !this.visible;
|
|
this.texture.updateLayerChanges(true);
|
|
Undo.finishEdit('Toggle layer visibility');
|
|
}
|
|
propertiesDialog() {
|
|
let dialog = new Dialog({
|
|
id: 'layer_properties',
|
|
title: this.name,
|
|
width: 660,
|
|
form: {
|
|
name: {label: 'generic.name', value: this.name},
|
|
opacity: {label: 'Opacity', type: 'range', value: this.opacity},
|
|
},
|
|
onConfirm: form_data => {
|
|
dialog.hide().delete();
|
|
if (
|
|
form_data.name != this.name
|
|
|| form_data.opacity != this.opacity
|
|
) {
|
|
Undo.initEdit({layers: [this]});
|
|
this.extend(form_data)
|
|
Blockbench.dispatchEvent('edit_layer_properties', {layer: this})
|
|
Undo.finishEdit('Edit layer properties');
|
|
}
|
|
},
|
|
onCancel() {
|
|
dialog.hide().delete();
|
|
}
|
|
})
|
|
dialog.show();
|
|
}
|
|
}
|
|
TextureLayer.prototype.menu = new Menu([
|
|
new MenuSeparator('settings'),
|
|
new MenuSeparator('copypaste'),
|
|
'copy',
|
|
'duplicate',
|
|
'delete',
|
|
new MenuSeparator('properties'),
|
|
{
|
|
icon: 'list',
|
|
name: 'menu.texture.properties',
|
|
click(layer) { layer.propertiesDialog()}
|
|
}
|
|
/**
|
|
* Merge
|
|
* Copy
|
|
* Duplicate
|
|
* Delete
|
|
*/
|
|
])
|
|
new Property(TextureLayer, 'string', 'name', {default: 'layer'});
|
|
new Property(TextureLayer, 'vector2', 'offset');
|
|
new Property(TextureLayer, 'number', 'opacity', {default: 100});
|
|
new Property(TextureLayer, 'boolean', 'visible', {default: true});
|
|
|
|
Object.defineProperty(TextureLayer, 'all', {
|
|
get() {
|
|
Texture.selected?.layers_enabled ? Texture.selected.layers : [];
|
|
}
|
|
})
|
|
Object.defineProperty(TextureLayer, 'selected', {
|
|
get() {
|
|
Texture.selected?.selected_layer;
|
|
}
|
|
})
|
|
|
|
SharedActions.add('delete', {
|
|
condition: () => Prop.active_panel == 'layers' && Texture.selected?.selected_layer,
|
|
run() {
|
|
if (Texture.selected.layers.length >= 2) {
|
|
Texture.selected?.selected_layer.remove(true);
|
|
}
|
|
}
|
|
})
|
|
SharedActions.add('duplicate', {
|
|
condition: () => Prop.active_panel == 'layers' && Texture.selected?.selected_layer,
|
|
run() {
|
|
let texture = Texture.selected;
|
|
let original = texture.getActiveLayer();
|
|
let copy = original.getUndoCopy(true);
|
|
copy.name += '-copy';
|
|
Undo.initEdit({textures: [texture]});
|
|
let layer = new TextureLayer(copy, texture);
|
|
texture.layers.push(layer);
|
|
layer.select();
|
|
Undo.finishEdit('Duplicate layer');
|
|
}
|
|
})
|
|
|
|
BARS.defineActions(() => {
|
|
new Action('create_empty_layer', {
|
|
icon: 'new_window',
|
|
category: 'layers',
|
|
condition: () => Modes.paint && Texture.selected && Texture.selected.layers_enabled,
|
|
click() {
|
|
let texture = Texture.selected;
|
|
Undo.initEdit({textures: [texture], bitmap: true});
|
|
let layer = new TextureLayer({
|
|
name: `layer #${texture.layers.length+1}`
|
|
}, texture);
|
|
layer.setSize(texture.width, texture.height);
|
|
texture.layers.push(layer);
|
|
layer.select();
|
|
Undo.finishEdit('Create empty layer');
|
|
BARS.updateConditions();
|
|
}
|
|
})
|
|
new Action('enable_texture_layers', {
|
|
icon: 'library_add_check',
|
|
category: 'layers',
|
|
condition: () => Texture.selected && !Texture.selected.layers_enabled,
|
|
click() {
|
|
if (!Modes.paint) {
|
|
Modes.options.paint.select();
|
|
}
|
|
let texture = Texture.selected;
|
|
texture.activateLayers(true);
|
|
}
|
|
})
|
|
new NumSlider('layer_opacity', {
|
|
category: 'layers',
|
|
condition: () => Modes.paint && Texture.selected && Texture.selected.layers_enabled && Texture.selected.getActiveLayer(),
|
|
settings: {
|
|
min: 0, max: 100, default: 100,
|
|
show_bar: true
|
|
},
|
|
getInterval(event) {
|
|
return 1;
|
|
},
|
|
get() {
|
|
return Texture.selected.getActiveLayer().opacity;
|
|
},
|
|
change(modify) {
|
|
let layer = Texture.selected.getActiveLayer();
|
|
layer.opacity = Math.clamp(modify(layer.opacity), 0, 100);
|
|
Texture.selected.updateLayerChanges();
|
|
},
|
|
onBefore() {
|
|
Undo.initEdit({layers: [Texture.selected.getActiveLayer()]});
|
|
},
|
|
onAfter() {
|
|
Undo.finishEdit('Change layer opacity');
|
|
Texture.selected.updateLayerChanges(true);
|
|
}
|
|
})
|
|
})
|
|
|
|
Interface.definePanels(function() {
|
|
Vue.component('texture-layer-icon', {
|
|
props: {
|
|
layer: TextureLayer
|
|
},
|
|
template: '<div class="layer_icon_wrapper"></div>',
|
|
mounted() {
|
|
this.$el.append(this.layer.canvas);
|
|
}
|
|
})
|
|
function eventTargetToLayer(target, texture) {
|
|
let target_node = target;
|
|
let i = 0;
|
|
while (target_node && target_node.classList && !target_node.classList.contains('texture_layer')) {
|
|
if (i < 3 && target_node) {
|
|
target_node = target_node.parentNode;
|
|
i++;
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
return [texture.layers.find(layer => layer.uuid == target_node.attributes.layer_id.value), target_node];
|
|
}
|
|
function getOrder(loc, obj) {
|
|
if (!obj) {
|
|
return;
|
|
} else {
|
|
if (loc < 16) return -1;
|
|
return 1;
|
|
}
|
|
}
|
|
new Panel('layers', {
|
|
icon: 'layers',
|
|
growable: true,
|
|
condition: () => Modes.paint && Texture.selected && Texture.selected.layers_enabled,
|
|
default_position: {
|
|
slot: 'left_bar',
|
|
float_position: [0, 0],
|
|
float_size: [300, 300],
|
|
height: 300
|
|
},
|
|
toolbars: [
|
|
new Toolbar('layers', {
|
|
children: [
|
|
'create_empty_layer',
|
|
'enable_texture_layers',
|
|
]
|
|
})
|
|
],
|
|
component: {
|
|
name: 'panel-layers',
|
|
data() { return {
|
|
layers: [],
|
|
}},
|
|
methods: {
|
|
openMenu(event) {
|
|
Interface.Panels.layers.menu.show(event)
|
|
},
|
|
dragLayer(e1) {
|
|
if (getFocusedTextInput()) return;
|
|
if (e1.button == 1 || e1.button == 2) return;
|
|
convertTouchEvent(e1);
|
|
|
|
let texture = Texture.selected;
|
|
if (!texture) return;
|
|
let [layer] = eventTargetToLayer(e1.target, texture);
|
|
if (!layer || layer.locked) return;
|
|
|
|
let active = false;
|
|
let helper;
|
|
let timeout;
|
|
let drop_target, drop_target_node, order;
|
|
let last_event = e1;
|
|
|
|
function move(e2) {
|
|
convertTouchEvent(e2);
|
|
let offset = [
|
|
e2.clientX - e1.clientX,
|
|
e2.clientY - e1.clientY,
|
|
]
|
|
if (!active) {
|
|
let distance = Math.sqrt(Math.pow(offset[0], 2) + Math.pow(offset[1], 2))
|
|
if (Blockbench.isTouch) {
|
|
if (distance > 20 && timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
} else {
|
|
document.getElementById('layers_list').scrollTop += last_event.clientY - e2.clientY;
|
|
}
|
|
} else if (distance > 6) {
|
|
active = true;
|
|
}
|
|
} else {
|
|
if (e2) e2.preventDefault();
|
|
|
|
if (Menu.open) Menu.open.hide();
|
|
|
|
if (!helper) {
|
|
helper = document.createElement('div');
|
|
helper.id = 'animation_drag_helper';
|
|
let icon = document.createElement('i'); icon.className = 'material-icons'; icon.innerText = 'image'; helper.append(icon);
|
|
let span = document.createElement('span'); span.innerText = layer.name; helper.append(span);
|
|
document.body.append(helper);
|
|
}
|
|
helper.style.left = `${e2.clientX}px`;
|
|
helper.style.top = `${e2.clientY}px`;
|
|
|
|
// drag
|
|
$('.drag_hover').removeClass('drag_hover');
|
|
$('.texture_layer[order]').attr('order', null);
|
|
|
|
let target = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
[drop_target, drop_target_node] = eventTargetToLayer(target, texture);
|
|
if (drop_target) {
|
|
var location = e2.clientY - $(drop_target_node).offset().top;
|
|
order = getOrder(location, drop_target)
|
|
drop_target_node.setAttribute('order', order)
|
|
drop_target_node.classList.add('drag_hover');
|
|
}
|
|
}
|
|
last_event = e2;
|
|
}
|
|
function off(e2) {
|
|
if (helper) helper.remove();
|
|
removeEventListeners(document, 'mousemove touchmove', move);
|
|
removeEventListeners(document, 'mouseup touchend', off);
|
|
$('.drag_hover').removeClass('drag_hover');
|
|
$('.texture_layer[order]').attr('order', null);
|
|
if (Blockbench.isTouch) clearTimeout(timeout);
|
|
|
|
if (active && !open_menu) {
|
|
convertTouchEvent(e2);
|
|
let target = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
[target_layer] = eventTargetToLayer(target, texture);
|
|
if (!target_layer || target_layer == layer ) return;
|
|
|
|
let index = texture.layers.indexOf(target_layer);
|
|
|
|
if (index == -1) return;
|
|
if (texture.layers.indexOf(layer) < index) index--;
|
|
if (order == -1) index++;
|
|
if (texture.layers[index] == layer) return;
|
|
|
|
Undo.initEdit({textures: [texture]});
|
|
|
|
texture.layers.remove(layer);
|
|
texture.layers.splice(index, 0, layer);
|
|
|
|
texture.updateLayerChanges(true);
|
|
Undo.finishEdit('Reorder layers');
|
|
}
|
|
}
|
|
|
|
if (Blockbench.isTouch) {
|
|
timeout = setTimeout(() => {
|
|
active = true;
|
|
move(e1);
|
|
}, 320)
|
|
}
|
|
|
|
addEventListeners(document, 'mousemove touchmove', move, {passive: false});
|
|
addEventListeners(document, 'mouseup touchend', off, {passive: false});
|
|
}
|
|
},
|
|
template: `
|
|
<ul
|
|
id="layers_list"
|
|
class="list mobile_scrollbar"
|
|
@contextmenu.stop.prevent="openMenu($event)"
|
|
@mousedown="dragLayer($event)"
|
|
@touchstart="dragLayer($event)"
|
|
>
|
|
<li
|
|
v-for="layer in layers"
|
|
:class="{ selected: layer.selected }"
|
|
:key="layer.uuid"
|
|
:layer_id="layer.uuid"
|
|
class="texture_layer"
|
|
@click.stop="layer.select()"
|
|
@dblclick.stop="layer.propertiesDialog()"
|
|
@contextmenu.prevent.stop="layer.showContextMenu($event)"
|
|
>
|
|
<texture-layer-icon :layer="layer" />
|
|
|
|
<label>
|
|
{{ layer.name }}
|
|
</label>
|
|
|
|
<div class="in_list_button" @click.stop="layer.toggleVisibility()">
|
|
<i v-if="layer.visible" class="material-icons">visibility</i>
|
|
<i v-else class="material-icons toggle_disabled">visibility_off</i>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
`
|
|
},
|
|
menu: new Menu([
|
|
'create_empty_layer',
|
|
])
|
|
})
|
|
})
|