Published
Edited
Apr 4, 2022
2 stars
Simple Minecraft Skin Editor
Also listed in…
Kappanneo
Tools
Insert cell
Insert cell
Insert cell
Insert cell
viewof pixels = pixel_selector(input_image)
Insert cell
pixels
Insert cell
viewof skin = skin_editor(pixels);
Insert cell
pixels
Insert cell
edited_skin = skin // quick PNG download in the drop menu on the left of the picture C;
Insert cell
Insert cell
pixel_selector = (img) => {
// extract pixel data from image
const context = DOM.context2d(w, h, 1);
context.drawImage(img, 0, 0);
const img_data = context.canvas.getContext("2d").getImageData(0, 0, w, h).data;
const pxls = Array.from({ length: img_data.length / 4 }).map((_, i) =>({
color: d3.rgb(
img_data[4 * i + 0],
img_data[4 * i + 1],
img_data[4 * i + 2]
).hex(),
opacity:img_data[4 * i + 3]/255,
selected: false,
x:(w*Math.floor(i/(w*h)) + i%w),
y:(Math.floor(i/w)%h)
}));
// default pixel selection
pxls.forEach( p => {
p.selected = p.opacity? true: false;
})
// view
const svg = html`<svg>`;
const action_radio_button = radio({options:["Add","Remove"],value:"Remove"});
const element = html`${svg}${action_radio_button}`;
function update() {
element.value = pxls.filter( p => p.selected ).map(
({color,opacity,x,y}) => ({color:color,opacity:opacity,x:x,y:y})
);
element.dispatchEvent(new CustomEvent("input"));
}
// render svg
const heigth = d3.min([500,(width/w)*h]);
d3.select(svg)
.attr("width",width)
.attr("height",heigth);
let rects = d3.select(svg).selectAll("rect");
let active = false;
const l = d3.min([heigth/h, width/w]);
function render() {
rects = rects.data(pxls)
.join(
enter => enter.append("rect")
.attr("x", p => p.x * l)
.attr("y", p => p.y * l)
.attr("height", l)
.attr("width", l)
.attr("stroke-width","1px")
.attr("stroke", "black")
.attr("fill", p => p.color )
.on("click", p => {
p.selected = action_radio_button.value === "Add"? true: false;
p.opacity = action_radio_button.value === "Add"? 1: 0;
render();
update();
})
.on("mouseover", p => {
if(active){
p.selected = action_radio_button.value === "Add"? true: false;
p.opacity = action_radio_button.value === "Add"? 1: 0;
render();
}
}),
update => update
.attr("stroke-opacity", p => p.selected? 1: 0.2 )
.attr("fill-opacity", p => p.selected? p.opacity: p.opacity/5 ),
exit => exit.remove()
)
}
svg.onmousedown = () => {
active = true;
};
svg.onmouseup = () => {
active = false;
update();
};
render(); // create view
render(); // update view
update(); // update value
return element;
}
Insert cell
skin_editor = (pxls) => {
// view
const svg = html`<svg>`;
const color_picker = color();
const borders_checkbox = checkbox(["Borders"]);
const action_radio = radio({options:["Draw","Pick","Alpha"],value:"Draw"});
const alpha_field = number({placeholder:"opacity",value:1,min:0,max:1,step:0.1});
const undo_button = button("Undo");
const element = html`
${svg}
${color_picker}
${alpha_field}
${action_radio}
${borders_checkbox}
${undo_button}
`;
function update() {
const data = Uint8ClampedArray.from({length: w * h * 4});
const context = DOM.context2d(w, h, 1);
pxls.forEach( p => {
const c = d3.color(p.color);
data[4 * (p.y*64 + p.x) + 0] = c.r;
data[4 * (p.y*64 + p.x) + 1] = c.g;
data[4 * (p.y*64 + p.x) + 2] = c.b;
data[4 * (p.y*64 + p.x) + 3] = p.opacity*255;
})
context.canvas.value = new ImageData(data, w, h);
context.putImageData(context.canvas.value, 0, 0);
element.value = context.canvas;
element.dispatchEvent(new CustomEvent("input"));
};
// render svg
const heigth = d3.min([500,(width/w)*h]);
d3.select(svg)
.attr("width",width)
.attr("height",heigth);
let rects = d3.select(svg).selectAll("rect");
let active = false;
let previous_colors = [];
const l = d3.min([heigth/h, width/w]);
function render(){
rects = rects.data(pxls)
.join(
enter => enter.append("rect")
.attr("height", l)
.attr("width", l)
.attr("stroke-width","1px")
.attr("x", p => p.x * l)
.attr("y", p => p.y * l)
.on("click", (p,i) => {
if(action_radio.value === "Draw"){
p.color = color_picker.value;
render();
update();
}
else if(action_radio.value === "Alpha"){
p.opacity = alpha_field.value;
render();
update();
}
else if (action_radio.value === "Pick") {
color_picker.reset()
color_picker.value = p.color;
alpha_field.value = p.opacity;
}
})
.on("mouseover", (p,i) => {
if(active && action_radio.value === "Draw") {
p.color = color_picker.value;
render();
}
else if(active && action_radio.value === "Alpha") {
p.opacity = alpha_field.value;
render();
}
})
,
update => update
.attr("fill", p => p.color )
.attr("fill-opacity", p => p.opacity )
.attr("stroke", p => borders_checkbox.value? "black": p.color )
.attr("stroke-opacity", p => borders_checkbox.value? "black": p.opacity ),
exit => exit.remove()
);
}
svg.onmousedown = () => {
active = true;
previous_colors.push(pxls.map( p => p.color ));
};
svg.onmouseup = () => {
active = false;
update();
};
borders_checkbox.oninput = () => render();
undo_button.onclick = () => {
const colors = previous_colors.pop();
pxls.forEach( (p,i) => {
p.color = colors[i];
})
render();
update();
}

render(); // create view
render(); // update view
update(); // update value
return element;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {select,color,checkbox, radio, button, number} from "@jashkenas/inputs"
Insert cell
import {imageInput} from "@mbostock/file-input"
Insert cell
d3 = require("d3@5")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more