|
<!doctype html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Skin Editor</title> |
|
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, width=device-width"> |
|
<style> |
|
* |
|
{ |
|
box-sizing: border-box; |
|
margin: 0px; |
|
padding: 0px; |
|
} |
|
|
|
img |
|
{ |
|
image-rendering: optimizeSpeed; |
|
image-rendering: -webkit-optimize-contrast; |
|
} |
|
|
|
html, |
|
body |
|
{ |
|
background-color: #eee; |
|
font: 400 14px Helvetica Neue; |
|
overflow: hidden; |
|
} |
|
|
|
.hidden |
|
{ |
|
display: none; |
|
} |
|
|
|
/* Color bar */ |
|
|
|
.colors |
|
{ |
|
position: fixed; |
|
top: 0px; |
|
right: 0px; |
|
|
|
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.425); |
|
border-radius: 0px 0px 0px 8px; |
|
overflow: hidden; |
|
} |
|
|
|
.color-result |
|
{ |
|
float: left; |
|
width: 60px; |
|
height: 36px; |
|
} |
|
|
|
#hide |
|
{ |
|
background-color: red; |
|
border: 0px; |
|
color: white; |
|
font-size: 14px; |
|
font-weight: bold; |
|
|
|
float: left; |
|
height: 36px; |
|
padding: 0px 24px; |
|
} |
|
|
|
.color-input |
|
{ |
|
border: 0px solid #eee; |
|
border-radius: 0px; |
|
font-size: 14px; |
|
|
|
padding: 6px 6px; |
|
height: 36px; |
|
width: 176px; |
|
} |
|
|
|
/* Tool bar */ |
|
|
|
.color |
|
{ |
|
background-color: #000; |
|
border-bottom: 3px solid #fff; |
|
|
|
float: left; |
|
} |
|
|
|
.bar |
|
{ |
|
border-radius: 0px 0px 8px 0px; |
|
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.425); |
|
overflow: hidden; |
|
|
|
float: left; |
|
} |
|
|
|
.second |
|
{ |
|
border-radius: 0px 0px 8px 8px; |
|
|
|
margin-left: 2em; |
|
} |
|
|
|
.bar:after |
|
{ |
|
clear: both; |
|
content: ""; |
|
display: block; |
|
height: 0px; |
|
} |
|
|
|
.toolbar |
|
{ |
|
position: fixed; |
|
top: 0px; |
|
left: 0px; |
|
right: 0px; |
|
} |
|
|
|
.toolbar .color |
|
{ |
|
padding: 8px 12px; |
|
} |
|
|
|
.pencil |
|
{ |
|
color: #000; |
|
background-color: #ff0; |
|
border-color: #ff0; |
|
} |
|
|
|
.eraser |
|
{ |
|
background-color: #eee; |
|
border-color: #eee; |
|
} |
|
|
|
.picker |
|
{ |
|
color: #fff; |
|
background-color: #444; |
|
border-color: #444; |
|
} |
|
|
|
.move |
|
{ |
|
color: #fff; |
|
background-color: deeppink; |
|
border-color: deeppink; |
|
} |
|
|
|
.import |
|
{ |
|
color: #fff; |
|
background-color: #08f; |
|
border-color: #08f; |
|
} |
|
|
|
.reset |
|
{ |
|
color: #fff; |
|
background-color: #e11; |
|
border-color: #e11; |
|
} |
|
|
|
.toolbar .bar:not(.second) :not(.selected) |
|
{ |
|
border-color: #fff; |
|
} |
|
|
|
/* Canvas */ |
|
|
|
#canvas |
|
{ |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
|
|
background-color: #fff; |
|
border: 1px solid #ddd; |
|
|
|
margin-left: -320px; |
|
margin-top: -160px; |
|
} |
|
|
|
#preview |
|
{ |
|
position: fixed; |
|
bottom: 0px; |
|
left: 0px; |
|
|
|
background-color: rgba(0, 0, 0, 0.2); |
|
|
|
height: 192px; |
|
width: 204px; |
|
} |
|
|
|
#avatar |
|
{ |
|
position: fixed; |
|
bottom: 200px; |
|
|
|
background-color: rgba(0, 0, 0, 0.2); |
|
|
|
height: 64px; |
|
width: 128px; |
|
} |
|
|
|
#red, |
|
#green, |
|
#blue |
|
{ |
|
position: absolute; |
|
right: 8px; |
|
} |
|
|
|
#red |
|
{ |
|
top: 44px; |
|
border-radius: 8px 8px 0px 0px; |
|
} |
|
|
|
#green |
|
{ |
|
top: 74px; |
|
} |
|
|
|
#blue |
|
{ |
|
top: 104px; |
|
border-radius: 0px 0px 8px 8px; |
|
} |
|
|
|
#file |
|
{ |
|
visibility: hidden; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<canvas id="canvas" width="640" height="320"></canvas> |
|
<img id="preview"> |
|
<img id="avatar"> |
|
|
|
<div class="colors"> |
|
<button id="hide">X</button> |
|
<div class="color-result"></div> |
|
<input class="color-input" placeholder="Hex or named color" type="color"> |
|
</div> |
|
|
|
<canvas id="red" width="220" height="30"></canvas> |
|
<canvas id="green" width="220" height="30"></canvas> |
|
<canvas id="blue" width="220" height="30"></canvas> |
|
|
|
<div class="toolbar"> |
|
<div class="bar"> |
|
<div class="color pencil" data-tool="pencil">Pencil</div> |
|
<div class="color eraser" data-tool="eraser">Eraser</div> |
|
<div class="color picker" data-tool="picker">Picker</div> |
|
<div class="color move" data-tool="move">Move</div> |
|
</div> |
|
|
|
<div class="bar second"> |
|
<div class="color import">Import</div> |
|
<div class="color reset">Reset</div> |
|
</div> |
|
|
|
<input id="file" type="file"> |
|
</div> |
|
<script>var transparent = [0, 0, 0, 0]; |
|
transparent.rgb = to_color(transparent); |
|
|
|
/* Utils */ |
|
function to_a (list) { |
|
return Array.prototype.slice.call(list); |
|
} |
|
|
|
function eStack (e) { |
|
var stack = e.stack.split("\n").map(function (str) { |
|
return str.substr(0, str.indexOf("@")) + "@localhost"; |
|
}); |
|
|
|
return stack.join("\n"); |
|
} |
|
|
|
/* DOM functions */ |
|
function $ (s, c) { |
|
return (c || document).querySelector(s); |
|
} |
|
|
|
function $$ (s, c) { |
|
return to_a((c || document).querySelectorAll(s)); |
|
} |
|
|
|
function on (n, e, f) { |
|
n.addEventListener(e, f); |
|
} |
|
|
|
/* Canvas class */ |
|
function Canvas (w, h) { |
|
this.w = w; |
|
this.h = h; |
|
this.reset(); |
|
} |
|
|
|
Canvas.prototype = { |
|
reset: function () { |
|
this.map = []; |
|
|
|
for (var x = 0; x < this.w; x ++) { |
|
this.map[x] = []; |
|
|
|
for (var y = 0; y < this.h; y ++) { |
|
this.map[x][y] = transparent; |
|
} |
|
} |
|
}, |
|
|
|
scale: function (w, h) { |
|
return Math.floor(Math.min(w / this.w, h / this.h)) || 1 |
|
}, |
|
|
|
draw: function (x, y, color) { |
|
color.rgb = to_color(color); |
|
this.map[x][y] = color; |
|
}, |
|
|
|
render: function (ctx, w, h) { |
|
var scale = this.scale(w, h); |
|
|
|
for (var x = 0; x < this.w; x ++) { |
|
for (var y = 0; y < this.h; y ++) { |
|
this.renderPixel(ctx, x, y, scale); |
|
} |
|
} |
|
}, |
|
|
|
renderPixel: function (ctx, x, y, scale, x2, y2) { |
|
ctx.fillStyle = this.map[x][y].rgb; |
|
|
|
x2 = x2 === undefined ? x : x2; |
|
y2 = y2 === undefined ? y : y2; |
|
|
|
ctx.clearRect(x2 * scale, y2 * scale, scale, scale); |
|
ctx.fillRect(x2 * scale, y2 * scale, scale, scale); |
|
}, |
|
|
|
fromImageData: function (img) { |
|
for (var i = 0, c = img.data.length; i < c; i += 4) { |
|
var x = (i / 4) % img.width, |
|
y = Math.floor((i / 4) / img.width); |
|
|
|
var r = img.data[i], |
|
g = img.data[i + 1], |
|
b = img.data[i + 2], |
|
a = img.data[i + 3]; |
|
|
|
this.draw(x, y, [r, g, b, a]); |
|
} |
|
} |
|
}; |
|
|
|
function to_color (color) { |
|
var r = color[0] >> 0, |
|
g = color[1] >> 0, |
|
b = color[2] >> 0, |
|
a = color[3] >> 0; |
|
|
|
return "rgba(" + r + "," + g + "," + b + "," + a + ")"; |
|
} |
|
|
|
function Slide (picker, canvas, index) { |
|
this.picker = picker; |
|
this.ctx = canvas.getContext("2d"); |
|
this.index = index; |
|
|
|
this.initEvents(canvas); |
|
this.generateGradient(); |
|
} |
|
|
|
Slide.prototype = { |
|
initEvents: function (canvas) { |
|
var self = this, |
|
callback = function (e) { self.click(e, canvas); }; |
|
|
|
on(canvas, "touchstart", callback); |
|
on(canvas, "touchmove", callback); |
|
this.canvas = canvas; |
|
}, |
|
|
|
click: function (e, canvas) { |
|
var value = e.touches[0].pageX - canvas.offsetLeft; |
|
|
|
value = (15 + value) / (canvas.width - 30); |
|
|
|
this.picker.setComponent(value * 255, this.index); |
|
}, |
|
|
|
generateGradient: function () { |
|
var color = this.picker.color.slice(); |
|
|
|
color[this.index] = 0; |
|
this.gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, 0); |
|
this.gradient.addColorStop(0, to_color(color)); |
|
color[this.index] = 255; |
|
this.gradient.addColorStop(1, to_color(color)); |
|
}, |
|
|
|
render: function () { |
|
var w = this.canvas.width, |
|
h = this.canvas.height; |
|
|
|
var ctx = this.ctx, |
|
value = this.picker.color[this.index]; |
|
|
|
var x = 15 + (value / 255) * (w - 30); |
|
|
|
ctx.fillStyle = this.gradient; |
|
ctx.fillRect(0, 0, w, h); |
|
|
|
ctx.strokeStyle = "rgba(255,255,255,0.5)"; |
|
ctx.lineWidth = 1.6; |
|
ctx.beginPath(); |
|
ctx.arc(x, 15, 12, 0, Math.PI * 2); |
|
ctx.stroke(); |
|
} |
|
}; |
|
|
|
function Picker (red, green, blue) { |
|
this.color = [0, 0, 0, 1]; |
|
|
|
this.red = new Slide(this, red, 0); |
|
this.green = new Slide(this, green, 1); |
|
this.blue = new Slide(this, blue, 2); |
|
} |
|
|
|
Picker.prototype = { |
|
render: function () { |
|
this.red.render(); |
|
this.green.render(); |
|
this.blue.render(); |
|
}, |
|
|
|
setColor: function (r, g, b) { |
|
this.color = arguments.length == 3 ? [r, g, b, 1] : r; |
|
|
|
input.value = to_color(this.color); |
|
color.style.backgroundColor = to_color(this.color); |
|
|
|
this.red.generateGradient(); |
|
this.green.generateGradient(); |
|
this.blue.generateGradient(); |
|
|
|
this.render(); |
|
}, |
|
|
|
setComponent: function (value, index) { |
|
this.color[index] = value > 255 ? 255 : (value < 0 ? 0 : value); |
|
this.setColor(this.color.slice()); |
|
} |
|
}; |
|
|
|
/* States */ |
|
var app = { |
|
picker: new Picker($("#red"), $("#green"), $("#blue")), |
|
tools: { |
|
pencil: function (x, y) { |
|
app.canvas.draw(x, y, app.picker.color); |
|
}, |
|
eraser: function (x, y) { |
|
app.canvas.draw(x, y, transparent); |
|
}, |
|
picker: function (x, y) { |
|
setColor(app.canvas.map[x][y].rgb, true); |
|
} |
|
}, |
|
tool: "pencil", |
|
canvas: new Canvas(64, 32) |
|
}; |
|
|
|
/* HTML */ |
|
var canvas = $("#canvas"), |
|
preview = $("#preview"), |
|
avatar = $("#avatar"), |
|
toolbar = $$("[data-tool]"), |
|
color = $(".color-result"), |
|
input = $(".color-input"), |
|
importFile = $("#file"), |
|
hide = $("#hide"); |
|
|
|
var parseCanvas = document.createElement("canvas"); |
|
|
|
parseCanvas.width = parseCanvas.height = 1; |
|
|
|
/* Bootstrap */ |
|
var prevent = function (e) { |
|
e.preventDefault(); |
|
}; |
|
|
|
on(document, "touchmove", prevent); |
|
on(canvas, "touchend", prevent); |
|
|
|
var sx, sy, |
|
omx, omy; |
|
|
|
on(canvas, "touchstart", function (e) { |
|
if (app.tool != "move") return drawPixel(e); |
|
|
|
sx = e.touches[0].pageX; |
|
sy = e.touches[0].pageY; |
|
|
|
omx = parseInt(canvas.style.marginLeft) || 0; |
|
omy = parseInt(canvas.style.marginTop) || 0; |
|
}); |
|
|
|
on(canvas, "touchmove", function (e) { |
|
if (app.tool != "move") return drawPixel(e); |
|
|
|
var ox = sx - e.touches[0].pageX, |
|
oy = sy - e.touches[0].pageY; |
|
|
|
canvas.style.marginLeft = (omx - ox) + "px"; |
|
canvas.style.marginTop = (omy - oy) + "px"; |
|
}); |
|
|
|
var sw, sh; |
|
|
|
on(document, "gesturestart", function (e) { |
|
prevent(e); |
|
sw = canvas.width; |
|
sh = canvas.height; |
|
}); |
|
|
|
on(document, "gesturechange", function (e) { |
|
if (app.tool != "move") return; |
|
|
|
var w = Math.floor(sw * e.scale), |
|
h = Math.floor(sh * e.scale); |
|
|
|
w -= w % 64; |
|
h -= h % 32; |
|
|
|
canvas.width = w; |
|
canvas.height = h; |
|
}); |
|
|
|
on(document, "gestureend", function () { |
|
drawCanvas(canvas, true); |
|
}); |
|
|
|
on(input, "change", function () { |
|
setColor(this.value); |
|
}); |
|
|
|
on(importFile, "change", function () { |
|
var reader = new FileReader(); |
|
|
|
reader.onload = function (e) { |
|
var image = new Image(); |
|
|
|
image.onload = function () { |
|
pcanvas.width = 64; |
|
pcanvas.height = 32; |
|
|
|
var ctx = pcanvas.getContext("2d"); |
|
|
|
ctx.drawImage(image, 0, 0); |
|
app.canvas.fromImageData(ctx.getImageData(0, 0, 64, 32)); |
|
drawCanvas(canvas, true); |
|
}; |
|
|
|
image.src = e.target.result; |
|
}; |
|
|
|
reader.readAsDataURL(this.files[0]); |
|
}); |
|
|
|
on($(".import"), "click", function () { |
|
importFile.click(); |
|
}); |
|
|
|
on($(".reset"), "click", function () { |
|
if (!confirm("Are you sure you want to reset?")) return; |
|
|
|
app.canvas.reset(); |
|
drawCanvas(canvas, true); |
|
}); |
|
|
|
on(hide, "click", function () { |
|
app.picker.red.canvas.classList.toggle("hidden"); |
|
app.picker.green.canvas.classList.toggle("hidden"); |
|
app.picker.blue.canvas.classList.toggle("hidden"); |
|
}); |
|
|
|
toolbar.forEach(function (node) { |
|
on(node, "click", function () { |
|
setTool(node); |
|
}); |
|
}); |
|
|
|
setTool($(".pencil")); |
|
setColor("#000", true); |
|
drawCanvas(canvas, true); |
|
app.picker.render(); |
|
|
|
setInterval(updatePreview, 700); |
|
|
|
canvas.style.marginLeft = "-320px"; |
|
canvas.style.marginTop = "-160px"; |
|
|
|
/* Functions */ |
|
function setTool (node) { |
|
app.tool = node.dataset.tool; |
|
|
|
toolbar.forEach(function (tool) { |
|
tool.classList.toggle("selected", tool.dataset.tool == app.tool); |
|
}); |
|
} |
|
|
|
function setColor (clr, reset) { |
|
if (clr = parseColor(clr)) { |
|
app.picker.setColor(clr); |
|
|
|
if (reset) input.value = to_color(clr); |
|
} |
|
} |
|
|
|
function drawCanvas (canvas, grid) { |
|
app.canvas.render(canvas.getContext("2d"), canvas.width, canvas.height); |
|
|
|
if (grid) drawGrid(canvas.getContext("2d"), canvas.width, canvas.height); |
|
} |
|
|
|
function drawGrid(ctx, w, h) { |
|
var scale = app.canvas.scale(w, h); |
|
|
|
if (scale < 3) return; |
|
|
|
ctx.fillStyle = "#eee"; |
|
for (var i = 0; i < 64; i += 4) ctx.fillRect(i * scale, 0, 1, 32 * scale); |
|
for (var i = 0; i < 32; i += 4) ctx.fillRect(0, i * scale, 64 * scale, 1); |
|
} |
|
|
|
function drawPixel (e) { |
|
var x = e.touches[0].pageX - canvas.offsetLeft, |
|
y = e.touches[0].pageY - canvas.offsetTop, |
|
w = canvas.width, |
|
h = canvas.height; |
|
|
|
var scale = app.canvas.scale(w, h), |
|
nx = Math.floor(x / scale), |
|
ny = Math.floor(y / scale); |
|
|
|
if (nx < 0 || nx >= app.canvas.w || ny < 0 || ny >= app.canvas.h) return; |
|
|
|
app.tools[app.tool](nx, ny); |
|
app.canvas.renderPixel(canvas.getContext("2d"), nx, ny, scale); |
|
} |
|
|
|
var pcanvas = document.createElement("canvas"); |
|
|
|
function updatePreview () { |
|
pcanvas.width = 64; |
|
pcanvas.height = 32; |
|
|
|
drawCanvas(pcanvas); |
|
avatar.src = pcanvas.toDataURL("image/png"); |
|
|
|
var scale = 6; |
|
pcanvas.width = 204; |
|
pcanvas.height = 192; |
|
|
|
/* Front head */ |
|
drawRegion(pcanvas, 8, 8, 8, 8, 4, 0, scale); |
|
|
|
/* Arms */ |
|
drawRegion(pcanvas, 44, 20, 4, 12, 0, 8, scale); |
|
drawRegion(pcanvas, 44, 20, 4, 12, 12, 8, scale, true); |
|
|
|
/* Body */ |
|
drawRegion(pcanvas, 20, 20, 8, 12, 4, 8, scale); |
|
|
|
/* Legs */ |
|
drawRegion(pcanvas, 4, 20, 4, 12, 4, 20, scale); |
|
drawRegion(pcanvas, 4, 20, 4, 12, 8, 20, scale, true); |
|
|
|
/* Back head */ |
|
drawRegion(pcanvas, 24, 8, 8, 8, 22, 0, scale, true); |
|
|
|
/* Arms */ |
|
drawRegion(pcanvas, 52, 20, 4, 12, 18, 8, scale, true); |
|
drawRegion(pcanvas, 52, 20, 4, 12, 30, 8, scale); |
|
|
|
/* Body */ |
|
drawRegion(pcanvas, 32, 20, 8, 12, 22, 8, scale, true); |
|
|
|
/* Legs */ |
|
drawRegion(pcanvas, 12, 20, 4, 12, 22, 20, scale, true); |
|
drawRegion(pcanvas, 12, 20, 4, 12, 26, 20, scale); |
|
|
|
preview.src = pcanvas.toDataURL("image/png"); |
|
} |
|
|
|
function drawRegion(to, x, y, w, h, x2, y2, s, flip) { |
|
var ctx = to.getContext("2d"); |
|
|
|
for (var i = 0; i < w; i ++) { |
|
for (var j = 0; j < h; j ++) { |
|
app.canvas.renderPixel(ctx, i + x, j + y, s, (flip ? w - i - 1 : i) + x2, j + y2); |
|
} |
|
} |
|
} |
|
|
|
function parseColor (color) { |
|
var ctx = parseCanvas.getContext("2d"); |
|
|
|
ctx.fillStyle = color; |
|
ctx.fillRect(0, 0, 1, 1); |
|
|
|
var d = ctx.getImageData(0, 0, 1, 1).data; |
|
|
|
return [d[0], d[1], d[2], d[3] / 255]; |
|
}</script> |
|
</body> |
|
</html> |