blockbench/js/texturing/edit_texture.js
2023-05-28 15:13:00 +02:00

766 lines
26 KiB
JavaScript

BARS.defineActions(function() {
function getTextures() {
if (Texture.selected) {
return [Texture.selected];
} else {
return Texture.all;
}
}
let show_preview = true;
// Adjustments
new Action('invert_colors', {
icon: 'invert_colors',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, texture.width, texture.height);
ctx.filter = 'invert(1)';
ctx.drawImage(texture.img, 0, 0);
}, {no_undo: true});
})
Undo.finishEdit('Invert colors')
}
})
new Action('adjust_brightness_contrast', {
icon: 'brightness_high',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
let original_imgs = textures.map(tex => {
return tex.img.cloneNode();
})
Undo.initEdit({textures, bitmap: true});
new Dialog({
id: 'adjust_brightness_contrast',
title: 'action.adjust_brightness_contrast',
darken: false,
component: {
data() {return {
show_preview,
preview_changes: true,
brightness: 100,
contrast: 100,
textures
}},
methods: {
change() {
textures.forEach((texture, i) => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, texture.width, texture.height);
if (this.preview_changes) {
ctx.filter = `brightness(${this.brightness / 100}) contrast(${this.contrast / 100})`;
} else {
ctx.filter = `brightness(1.0) contrast(1.0)`;
}
ctx.drawImage(original_imgs[i], 0, 0);
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(canvas, 0, 0);
}, {no_undo: true, use_cache: true});
})
},
togglePreview() {
this.show_preview = show_preview = !this.show_preview;
}
},
template: `
<div>
<div class="texture_adjust_previews checkerboard" ref="preview_list" :class="{folded: !show_preview}">
<canvas v-for="(texture, i) in textures" :height="texture.height" :width="texture.width" ref="canvas" />
</div>
<div class="tool texture_adjust_preview_toggle" @click="togglePreview()"><i class="material-icons">{{ show_preview ? 'expand_more' : 'expand_less' }}</i></div>
<div class="bar slider_input_combo">
<input type="range" class="tool" min="0" max="200" step="1" v-model.number="brightness" @input="change()">
<numeric-input class="tool" :min="0" :max="200" :step="1" v-model.number="brightness" @input="change()" />
</div>
<div class="bar slider_input_combo">
<input type="range" class="tool" min="0" max="200" step="1" v-model.number="contrast" @input="change()">
<numeric-input class="tool" :min="0" :max="200" :step="1" v-model.number="contrast" @input="change()" />
</div>
<div class="bar button_bar_checkbox">
<input type="checkbox" v-model="preview_changes" id="checkbox_preview_changes" @change="change()">
<label for="checkbox_preview_changes">${tl('dialog.edit_texture.preview')}</label>
</div>
</div>
`,
mounted() {
textures.forEach((texture, i) => {
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(texture.img, 0, 0);
})
}
},
onConfirm() {
if (!this.content_vue.preview_changes) {
this.content_vue.preview_changes = true;
this.content_vue.change();
}
Undo.finishEdit('Adjust brightness and contrast');
},
onCancel() {
Undo.cancelEdit();
}
}).show();
}
})
new Action('adjust_saturation_hue', {
icon: 'fa-tint',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
let original_imgs = textures.map(tex => {
return tex.img.cloneNode();
})
Undo.initEdit({textures, bitmap: true});
new Dialog({
id: 'adjust_saturation_hue',
title: 'action.adjust_saturation_hue',
darken: false,
component: {
data() {return {
show_preview,
preview_changes: true,
saturation: 100,
hue: 0,
textures
}},
methods: {
change() {
textures.forEach((texture, i) => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, texture.width, texture.height);
if (this.preview_changes) {
ctx.filter = `saturate(${this.saturation / 100}) hue-rotate(${this.hue}deg)`;
} else {
ctx.filter = `brightness(1.0)`;
}
ctx.drawImage(original_imgs[i], 0, 0);
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(canvas, 0, 0);
}, {no_undo: true, use_cache: true});
})
},
togglePreview() {
this.show_preview = show_preview = !this.show_preview;
}
},
template: `
<div>
<div class="texture_adjust_previews checkerboard" :class="{folded: !show_preview}">
<canvas v-for="(texture, i) in textures" :height="texture.height" :width="texture.width" ref="canvas" />
</div>
<div class="tool texture_adjust_preview_toggle" @click="togglePreview()"><i class="material-icons">{{ show_preview ? 'expand_more' : 'expand_less' }}</i></div>
<div class="bar slider_input_combo">
<input type="range" class="tool" min="0" max="200" step="1" v-model.number="saturation" @input="change()">
<numeric-input class="tool" :min="0" :max="200" :step="1" v-model.number="saturation" @input="change()" />
</div>
<div class="bar slider_input_combo">
<input type="range" class="tool" min="-180" max="180" step="1" v-model.number="hue" @input="change()">
<numeric-input class="tool" :min="-180" :max="180" :step="1" v-model.number="hue" @input="change()" />
</div>
<div class="bar button_bar_checkbox">
<input type="checkbox" v-model="preview_changes" id="checkbox_preview_changes" @change="change()">
<label for="checkbox_preview_changes">${tl('dialog.edit_texture.preview')}</label>
</div>
</div>
`,
mounted() {
textures.forEach((texture, i) => {
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(texture.img, 0, 0);
})
}
},
onConfirm() {
if (!this.content_vue.preview_changes) {
this.content_vue.preview_changes = true;
this.content_vue.change();
}
Undo.finishEdit('Adjust saturation and hue');
},
onCancel() {
Undo.cancelEdit();
}
}).show();
}
})
new Action('adjust_curves', {
icon: 'fa-chart-line',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
let original_image_data = textures.map(tex => {
let canvas = Painter.getCanvas(tex);
let image_data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
image_data.original_data = image_data.data.slice();
return image_data;
})
Undo.initEdit({textures, bitmap: true});
let image_data = original_image_data[0];
let light_points = {};
let highest_light_point = 0;
for (let i = 0; i < 256; i++) {
light_points[i] = 0;
}
for (let i = 0; i < image_data.data.length; i += 4) {
let R = image_data.data[i+0];
let G = image_data.data[i+1];
let B = image_data.data[i+2];
let A = image_data.data[i+3];
if (A == 0) continue;
let brightness = (0.2126*R + 0.7152*G + 0.0722*B);
light_points[Math.round(brightness)] += 1;
highest_light_point = Math.max(highest_light_point, light_points[Math.round(brightness)]);
}
let curves = {
rgb: null,
r: null,
g: null,
b: null,
a: null,
};
function toCatmullRomBezier( points, tension = 0.5) {
if (points.length == 2) {
return `M${points[0][0]},${points[0][1]} L${points[1][0]},${points[1][1]}`
}
let tens = (tension !== 0) ? tension * 12 : 0.5 * 12
let floats = points.map(x => x.map(x => parseFloat(x)))
let firstMoveto = ['M' + floats[0][0] + ' ' + floats[0][1] + ' ']
let matrixPoints = floats.map((point, i, arr) => {
if (i == 0) {
return getMatrix([arr[i],arr[i],arr[i+1],arr[i+2]])
} else if (i == arr.length - 2) {
return getMatrix([arr[i-1],arr[i],arr[i+1],arr[i+1]])
} else {
return getMatrix([arr[i-1],arr[i],arr[i+1],arr[i+2]])
}
}).filter(mx => mx[3] !== undefined)
let matrixMathToBezier = matrixPoints.map(p => {
return [
{ x: p[1].x, y: p[1].y },
{ x: Math.clamp((-p[0].x + tens * p[1].x + p[2].x) / tens, p[1].x, p[2].x),
y: ((-p[0].y + tens * p[1].y + p[2].y) / tens)
},
{ x: Math.clamp(( p[1].x + tens * p[2].x - p[3].x) / tens, p[1].x, p[2].x),
y: ((p[1].y + tens * p[2].y - p[3].y) / tens)
},
{ x: p[2].x, y: p[2].y }
]
})
let toSVGNotation = matrixMathToBezier.map(bp => {
return "C" + bp[1].x + "," + bp[1].y + " " + bp[2].x + "," + bp[2].y + " " + bp[3].x + "," + bp[3].y + " "
})
return firstMoveto.concat(toSVGNotation).join(' ')
function getMatrix(arr) {
return arr.map(p => {
if (p !== undefined) { return { x : p[0], y : p[1] }}
})
}
}
new Dialog({
id: 'adjust_curves',
title: 'action.adjust_curves',
darken: false,
width: 460,
component: {
data() {return {
preview_changes: true,
light_data: '',
graph: 'rgb',
graphs: {
rgb: {data: '', points: [[0, 0], [1, 1]]},
r: {data: '', points: [[0, 0], [1, 1]]},
g: {data: '', points: [[0, 0], [1, 1]]},
b: {data: '', points: [[0, 0], [1, 1]]},
a: {data: '', points: [[0, 0], [1, 1]]},
},
height: 400,
width: 400,
textures
}},
methods: {
change() {
let values = {};
for (let key in this.graphs) {
let graph = this.graphs[key];
if (graph.points.length === 2 && graph.points[0].allEqual(0) && graph.points[1].allEqual(1)) {
delete curves[key];
} else {
let vectors = [];
values[key] = {};
graph.points.forEach(point => {
vectors.push(new THREE.Vector2(point[0], point[1]));
})
curves[key] = new THREE.SplineCurve(vectors);
}
}
textures.forEach((texture, i) => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
let image_data = original_image_data[i];
if (this.preview_changes) {
for (let i = 0; i < image_data.data.length; i += 4) {
let R = image_data.original_data[i+0]
let G = image_data.original_data[i+1]
let B = image_data.original_data[i+2]
let A = image_data.original_data[i+3]
let brightness = Math.round(0.2126*R + 0.7152*G + 0.0722*B);
let rgb = !curves.rgb ? brightness : (values.rgb[brightness] !== undefined ? values.rgb[brightness] : values.rgb[brightness] = curves.rgb.getPointAt(brightness / 255).y * 255);
let r = !curves.r ? Math.max(brightness, 1) : (values.r[brightness] !== undefined ? values.r[brightness] : values.r[brightness] = curves.r.getPointAt(brightness / 255).y * 255);
let g = !curves.g ? Math.max(brightness, 1) : (values.g[brightness] !== undefined ? values.g[brightness] : values.g[brightness] = curves.g.getPointAt(brightness / 255).y * 255);
let b = !curves.b ? Math.max(brightness, 1) : (values.b[brightness] !== undefined ? values.b[brightness] : values.b[brightness] = curves.b.getPointAt(brightness / 255).y * 255);
let a = !curves.a ? A : (values.a[A] !== undefined ? values.a[A] : values.a[A] = curves.a.getPointAt(A / 255).y * 255);
brightness = Math.max(brightness, 1);
image_data.data[i+0] = Math.max(R, 1) * (r / brightness) * (rgb / brightness);
image_data.data[i+1] = Math.max(G, 1) * (g / brightness) * (rgb / brightness);
image_data.data[i+2] = Math.max(B, 1) * (b / brightness) * (rgb / brightness);
image_data.data[i+3] = a;
}
} else {
for (let i = 0; i < image_data.data.length; i += 4) {
image_data.data[i+0] = image_data.original_data[i+0];
image_data.data[i+1] = image_data.original_data[i+1];
image_data.data[i+2] = image_data.original_data[i+2];
image_data.data[i+3] = image_data.original_data[i+3];
}
}
ctx.putImageData(image_data, 0, 0);
}, {no_undo: true, use_cache: true});
})
},
setGraph(graph) {
this.graph = graph;
this.updateGraph();
},
dragPoint(point, e1) {
let scope = this;
let {points} = this.graphs[this.graph];
let original_point = point.slice();
function drag(e2) {
if (point == points[0] || point == points.last()) {
point[0] = point == points[0] ? 0 : 1;
} else {
point[0] = Math.clamp(original_point[0] + (e2.clientX - e1.clientX) / scope.width, 0, 1);
}
point[1] = Math.clamp(original_point[1] - (e2.clientY - e1.clientY) / scope.height, 0, 1);
scope.updateGraph();
scope.change();
}
function stop() {
removeEventListeners(document, 'mousemove touchmove', drag);
removeEventListeners(document, 'mouseup touchend', stop);
}
addEventListeners(document, 'mousemove touchmove', drag);
addEventListeners(document, 'mouseup touchend', stop);
},
createNewPoint(event) {
if (event.target.id !== 'contrast_graph' || event.which == 3) return;
let point = [
(event.offsetX - 5) / this.width,
1 - ((event.offsetY - 5) / this.width),
]
let {points} = this.graphs[this.graph];
points.push(point);
this.updateGraph();
this.change();
this.dragPoint(point, event);
},
updateGraph(id = this.graph) {
let offset = 5;
let {points} = this.graphs[this.graph];
points.sort((a, b) => a[0] - b[0]);
this.graphs[id].data = toCatmullRomBezier(points.map(p => {
return [p[0] * this.width + offset, (1-p[1]) * this.height + offset];
}));
},
contextMenu(point, event) {
let {points} = this.graphs[this.graph];
if (point == points[0] || point == points.last()) return;
new Menu([{
id: 'remove',
name: 'Remove',
icon: 'clear',
click: () => {
let {points} = this.graphs[this.graph];
points.remove(point);
this.updateGraph();
this.change();
}
}]).open(event.target);
}
},
template: `
<div>
<div class="bar contrast_graph_selector">
<div @click="setGraph('rgb')" :class="{selected: graph == 'rgb'}">RGB</div>
<div @click="setGraph('r')" :class="{selected: graph == 'r'}">R</div>
<div @click="setGraph('g')" :class="{selected: graph == 'g'}">G</div>
<div @click="setGraph('b')" :class="{selected: graph == 'b'}">B</div>
<div @click="setGraph('a')" :class="{selected: graph == 'a'}">A</div>
</div>
<div id="contrast_graph" @mousedown="createNewPoint($event)" @touchstart="createNewPoint($event)">
<svg>
<path :class="{active: graph == 'r'}" :d="graphs.r.data" style="stroke: #ff0000;"></path>
<path :class="{active: graph == 'g'}" :d="graphs.g.data" style="stroke: #00ff00;"></path>
<path :class="{active: graph == 'b'}" :d="graphs.b.data" style="stroke: #3b3bff;"></path>
<path :class="{active: graph == 'a'}" :d="graphs.a.data" style="stroke: var(--color-text);"></path>
<path :class="{active: graph == 'rgb'}" :d="graphs.rgb.data"></path>
<polygon :points="light_data" />
</svg>
<div class="contrast_graph_point"
v-for="point in graphs[graph].points"
:style="{left: point[0] * width + 'px', top: (1-point[1]) * height + 'px'}"
@mousedown="dragPoint(point, $event)" @touchstart="dragPoint(point, $event)"
@contextmenu="contextMenu(point, $event)"
></div>
</div>
<div class="bar button_bar_checkbox">
<input type="checkbox" v-model="preview_changes" id="checkbox_preview_changes" @change="change()">
<label for="checkbox_preview_changes">${tl('dialog.edit_texture.preview')}</label>
</div>
</div>
`,
mounted() {
for (let key in this.graphs) {
this.updateGraph(key);
}
this.light_data = `${5},${this.height + 5}`;
for (let key in light_points) {
this.light_data += ` ${Math.round((key / 255) * this.width) + 5},${Math.round((1 - light_points[key] / highest_light_point) * this.height) + 5}`;
}
this.light_data += ` ${this.width + 5},${this.height + 5}`;
}
},
onConfirm() {
if (!this.content_vue.preview_changes) {
this.content_vue.preview_changes = true;
this.content_vue.change();
}
Undo.finishEdit('Invert colors');
},
onCancel() {
Undo.cancelEdit();
}
}).show();
}
})
new Action('adjust_opacity', {
icon: 'gradient',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
let original_imgs = textures.map(tex => {
return tex.img.cloneNode();
})
Undo.initEdit({textures, bitmap: true});
new Dialog({
id: 'adjust_opacity',
title: 'action.adjust_opacity',
darken: false,
component: {
data() {return {
show_preview,
preview_changes: true,
opacity: 100,
textures
}},
methods: {
change() {
textures.forEach((texture, i) => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, texture.width, texture.height);
if (this.preview_changes) {
ctx.filter = `opacity(${this.opacity}%)`;
ctx.drawImage(original_imgs[i], 0, 0);
if (this.opacity > 100 && this.preview_changes) {
ctx.filter = `opacity(${this.opacity-100}%)`;
ctx.drawImage(original_imgs[i], 0, 0);
}
} else {
ctx.filter = `opacity(100%)`;
ctx.drawImage(original_imgs[i], 0, 0);
}
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(canvas, 0, 0);
}, {no_undo: true, use_cache: true});
})
},
togglePreview() {
this.show_preview = show_preview = !this.show_preview;
}
},
template: `
<div>
<div class="texture_adjust_previews checkerboard" :class="{folded: !show_preview}">
<canvas v-for="(texture, i) in textures" :height="texture.height" :width="texture.width" ref="canvas" />
</div>
<div class="tool texture_adjust_preview_toggle" @click="togglePreview()"><i class="material-icons">{{ show_preview ? 'expand_more' : 'expand_less' }}</i></div>
<div class="bar slider_input_combo">
<input type="range" class="tool" min="0" max="200" step="0.1" v-model.number="opacity" @input="change()">
<numeric-input class="tool" style="width: 64px;" :min="0" :max="200" :step="0.1" v-model.number="opacity" @input="change()" />
</div>
<div class="bar button_bar_checkbox">
<input type="checkbox" v-model="preview_changes" id="checkbox_preview_changes" @change="change()">
<label for="checkbox_preview_changes">${tl('dialog.edit_texture.preview')}</label>
</div>
</div>
`,
mounted() {
textures.forEach((texture, i) => {
let ref_ctx = this.$refs.canvas[i].getContext('2d');
ref_ctx.clearRect(0, 0, texture.width, texture.height);
ref_ctx.drawImage(texture.img, 0, 0);
})
}
},
onConfirm() {
if (!this.content_vue.preview_changes) {
this.content_vue.preview_changes = true;
this.content_vue.change();
}
Undo.finishEdit('Adjust opacity');
},
onCancel() {
Undo.cancelEdit();
}
}).show();
}
})
// Effects
new Action('limit_to_palette', {
icon: 'blur_linear',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
var palette = {};
ColorPanel.palette.forEach(color => {
palette[color] = tinycolor(color);
})
Painter.scanCanvas(ctx, 0, 0, canvas.width, canvas.height, (x, y, pixel) => {
if (pixel[3] < 4) return;
let smallest_distance = Infinity;
let nearest_color = null;
let pixel_color = {_r: pixel[0], _g: pixel[1], _b: pixel[2]};
for (let key in palette) {
let color = palette[key];
let distance = colorDistance(color, pixel_color);
if (distance < smallest_distance) {
smallest_distance = distance;
nearest_color = color;
}
}
if (!nearest_color) return;
pixel[0] = nearest_color._r;
pixel[1] = nearest_color._g;
pixel[2] = nearest_color._b;
})
}, {no_undo: true});
})
Undo.finishEdit('Limit texture to palette')
}
})
new Action('clear_unused_texture_space', {
icon: 'cleaning_services',
category: 'textures',
condition: {modes: ['paint'], features: ['edit_mode'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
if (texture.frameCount > 1) {
Blockbench.showQuickMessage('This feature does not work on animated textures at the moment');
return;
}
texture.edit((canvas) => {
// todo: flipbook animation support
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
Outliner.elements.forEach(el => {
if (el instanceof Mesh) {
for (var fkey in el.faces) {
var face = el.faces[fkey];
if (face.vertices.length <= 2 || face.getTexture() !== texture) continue;
let matrix = face.getOccupationMatrix(true, [0, 0]);
for (let x in matrix) {
for (let y in matrix[x]) {
if (!matrix[x][y]) continue;
x = parseInt(x); y = parseInt(y);
ctx.rect(x, y, 1, 1);
}
}
}
} else if (el instanceof Cube) {
let factor_x = texture.width / Project.texture_width;
let factor_y = texture.height / Project.texture_height;
for (var fkey in el.faces) {
var face = el.faces[fkey];
if (face.getTexture() !== texture) continue;
let rect = face.getBoundingRect();
let canvasRect = [
Math.floor(rect.ax * factor_x),
Math.floor(rect.ay * factor_y),
Math.ceil(rect.bx * factor_x) - Math.floor(rect.ax * factor_x),
Math.ceil(rect.by * factor_y) - Math.floor(rect.ay * factor_y),
]
ctx.rect(...canvasRect);
}
}
})
ctx.clip();
ctx.drawImage(texture.img, 0, 0);
}, {no_undo: true});
})
Undo.finishEdit('Clear unused texture space')
}
})
// Transform
new Action('flip_texture_x', {
icon: 'icon-mirror_x',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.scale(-1, 1);
ctx.drawImage(texture.img, -canvas.width, 0);
}, {no_undo: true});
})
Undo.finishEdit('Flip texture X')
}
})
new Action('flip_texture_y', {
icon: 'icon-mirror_y',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.scale(1, -1);
ctx.drawImage(texture.img, 0, -canvas.height);
}, {no_undo: true});
})
Undo.finishEdit('Flip texture Y')
}
})
new Action('rotate_texture_cw', {
icon: 'rotate_right',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.rotate(Math.PI/2);
ctx.drawImage(texture.img, 0, -canvas.height);
}, {no_undo: true});
})
Undo.finishEdit('Rotate texture clockwise')
}
})
new Action('rotate_texture_ccw', {
icon: 'rotate_left',
category: 'textures',
condition: {modes: ['paint'], method: () => Texture.all.length},
click() {
let textures = getTextures();
Undo.initEdit({textures, bitmap: true});
textures.forEach(texture => {
texture.edit((canvas) => {
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.rotate(-Math.PI/2);
ctx.drawImage(texture.img, -canvas.width, 0);
}, {no_undo: true});
})
Undo.finishEdit('Rotate texture counter-clockwise')
}
})
new Action('resize_texture', {
icon: 'photo_size_select_large',
category: 'textures',
condition: () => Texture.all.length,
click() {
let texture = Texture.getDefault();
texture.resizeDialog();
}
})
})