You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1025 lines
27 KiB
1025 lines
27 KiB
<html>
|
|
<body>
|
|
|
|
<div>edges2cats</div>
|
|
<div id="edges2cats"></div>
|
|
|
|
<div>edges2shoes</div>
|
|
<div id="edges2shoes"></div>
|
|
|
|
<div>edges2handbags</div>
|
|
<div id="edges2handbags"></div>
|
|
|
|
<div>facades</div>
|
|
<div id="facades"></div>
|
|
|
|
<script src="deeplearn-0.3.15.js"></script>
|
|
<script>
|
|
|
|
var editor_background = new Image()
|
|
editor_background.src = "editor.png"
|
|
|
|
var DEBUG = false
|
|
var SIZE = 256
|
|
|
|
var editors = []
|
|
var request_in_progress = false
|
|
|
|
function main() {
|
|
var create_editor = function(config) {
|
|
var editor = new Editor(config)
|
|
var elem = document.getElementById(config.name)
|
|
elem.appendChild(editor.view.ctx.canvas)
|
|
editors.push(editor)
|
|
}
|
|
|
|
create_editor({
|
|
name: "edges2cats",
|
|
weights_url: "/models/edges2cats_AtoB.pict",
|
|
mode: "line",
|
|
clear: "#FFFFFF",
|
|
colors: {
|
|
line: "#000000",
|
|
eraser: "#ffffff",
|
|
},
|
|
draw: "#000000",
|
|
initial_input: "/edges2cats-input.png",
|
|
initial_output: "/edges2cats-output.png",
|
|
sheet_url: "/edges2cats-sheet.jpg",
|
|
})
|
|
|
|
create_editor({
|
|
name: "edges2shoes",
|
|
weights_url: "/models/edges2shoes_AtoB.pict",
|
|
mode: "line",
|
|
clear: "#FFFFFF",
|
|
colors: {
|
|
line: "#000000",
|
|
eraser: "#ffffff",
|
|
},
|
|
draw: "#000000",
|
|
initial_input: "/edges2shoes-input.png",
|
|
initial_output: "/edges2shoes-output.png",
|
|
sheet_url: "/edges2shoes-sheet.jpg",
|
|
})
|
|
|
|
create_editor({
|
|
name: "edges2handbags",
|
|
weights_url: "/models/edges2handbags_AtoB.pict",
|
|
mode: "line",
|
|
clear: "#FFFFFF",
|
|
colors: {
|
|
line: "#000000",
|
|
eraser: "#ffffff",
|
|
},
|
|
draw: "#000000",
|
|
initial_input: "/edges2handbags-input.png",
|
|
initial_output: "/edges2handbags-output.png",
|
|
sheet_url: "/edges2handbags-sheet.jpg",
|
|
})
|
|
|
|
create_editor({
|
|
name: "facades",
|
|
weights_url: "/models/facades_BtoA.pict",
|
|
mode: "rect",
|
|
colors: {
|
|
background: "#0006d9",
|
|
wall: "#0d3dfb",
|
|
door: "#a50000",
|
|
"window": "#0075ff",
|
|
"window sill": "#68f898",
|
|
"window head": "#1dffdd",
|
|
shutter: "#eeed28",
|
|
balcony: "#b8ff38",
|
|
trim: "#ff9204",
|
|
cornice: "#ff4401",
|
|
column: "#f60001",
|
|
entrance: "#00c9ff",
|
|
},
|
|
clear: "#0d3dfb",
|
|
draw: "#0075ff",
|
|
initial_input: "/facades-input.png",
|
|
initial_output: "/facades-output.png",
|
|
sheet_url: "/facades-sheet.jpg",
|
|
})
|
|
|
|
window.requestAnimationFrame(frame)
|
|
}
|
|
window.onload = main
|
|
|
|
function render() {
|
|
for (var i = 0; i < editors.length; i++) {
|
|
editors[i].render()
|
|
}
|
|
}
|
|
|
|
|
|
// model
|
|
|
|
var weights_cache = {}
|
|
function fetch_weights(path, progress_cb) {
|
|
return new Promise(function(resolve, reject) {
|
|
if (path in weights_cache) {
|
|
resolve(weights_cache[path])
|
|
return
|
|
}
|
|
|
|
var xhr = new XMLHttpRequest()
|
|
xhr.open("GET", path, true)
|
|
xhr.responseType = "arraybuffer"
|
|
|
|
xhr.onprogress = function(e) {
|
|
progress_cb(e.loaded, e.total)
|
|
}
|
|
|
|
xhr.onload = function(e) {
|
|
if (xhr.status != 200) {
|
|
reject("missing model")
|
|
return
|
|
}
|
|
var buf = xhr.response
|
|
if (!buf) {
|
|
reject("invalid arraybuffer")
|
|
return
|
|
}
|
|
|
|
var parts = []
|
|
var offset = 0
|
|
while (offset < buf.byteLength) {
|
|
var b = new Uint8Array(buf.slice(offset, offset+4))
|
|
offset += 4
|
|
var len = (b[0] << 24) + (b[1] << 16) + (b[2] << 8) + b[3]
|
|
parts.push(buf.slice(offset, offset + len))
|
|
offset += len
|
|
}
|
|
|
|
var shapes = JSON.parse((new TextDecoder("utf8")).decode(parts[0]))
|
|
var index = new Float32Array(parts[1])
|
|
var encoded = new Uint8Array(parts[2])
|
|
|
|
// decode using index
|
|
var arr = new Float32Array(encoded.length)
|
|
for (var i = 0; i < arr.length; i++) {
|
|
arr[i] = index[encoded[i]]
|
|
}
|
|
|
|
var weights = {}
|
|
var offset = 0
|
|
for (var i = 0; i < shapes.length; i++) {
|
|
var shape = shapes[i].shape
|
|
var size = shape.reduce((total, num) => total * num)
|
|
var values = arr.slice(offset, offset+size)
|
|
var dlarr = dl.Array1D.new(values, "float32")
|
|
weights[shapes[i].name] = dlarr.reshape(shape)
|
|
offset += size
|
|
}
|
|
weights_cache[path] = weights
|
|
resolve(weights)
|
|
}
|
|
xhr.send(null)
|
|
})
|
|
}
|
|
|
|
function model(input, weights) {
|
|
const math = dl.ENV.math
|
|
|
|
function preprocess(input) {
|
|
return math.subtract(math.multiply(input, dl.Scalar.new(2)), dl.Scalar.new(1))
|
|
}
|
|
|
|
function deprocess(input) {
|
|
return math.divide(math.add(input, dl.Scalar.new(1)), dl.Scalar.new(2))
|
|
}
|
|
|
|
function batchnorm(input, scale, offset) {
|
|
var moments = math.moments(input, [0, 1])
|
|
const varianceEpsilon = 1e-5
|
|
return math.batchNormalization3D(input, moments.mean, moments.variance, varianceEpsilon, scale, offset)
|
|
}
|
|
|
|
function conv2d(input, filter, bias) {
|
|
return math.conv2d(input, filter, bias, [2, 2], "same")
|
|
}
|
|
|
|
function deconv2d(input, filter, bias) {
|
|
var convolved = math.conv2dTranspose(input, filter, [input.shape[0]*2, input.shape[1]*2, filter.shape[2]], [2, 2], "same")
|
|
var biased = math.add(convolved, bias)
|
|
return biased
|
|
}
|
|
|
|
var preprocessed_input = preprocess(input)
|
|
|
|
var layers = []
|
|
|
|
var filter = weights["generator/encoder_1/conv2d/kernel"]
|
|
var bias = weights["generator/encoder_1/conv2d/bias"]
|
|
var convolved = conv2d(preprocessed_input, filter, bias)
|
|
layers.push(convolved)
|
|
|
|
for (var i = 2; i <= 8; i++) {
|
|
var scope = "generator/encoder_" + i.toString()
|
|
var filter = weights[scope + "/conv2d/kernel"]
|
|
var bias = weights[scope + "/conv2d/bias"]
|
|
var layer_input = layers[layers.length - 1]
|
|
var rectified = math.leakyRelu(layer_input, 0.2)
|
|
var convolved = conv2d(rectified, filter, bias)
|
|
var scale = weights[scope + "/batch_normalization/gamma"]
|
|
var offset = weights[scope + "/batch_normalization/beta"]
|
|
var normalized = batchnorm(convolved, scale, offset)
|
|
layers.push(normalized)
|
|
}
|
|
|
|
for (var i = 8; i >= 2; i--) {
|
|
if (i == 8) {
|
|
var layer_input = layers[layers.length - 1]
|
|
} else {
|
|
var skip_layer = i - 1
|
|
var layer_input = math.concat3D(layers[layers.length - 1], layers[skip_layer], 2)
|
|
}
|
|
var rectified = math.relu(layer_input)
|
|
var scope = "generator/decoder_" + i.toString()
|
|
var filter = weights[scope + "/conv2d_transpose/kernel"]
|
|
var bias = weights[scope + "/conv2d_transpose/bias"]
|
|
var convolved = deconv2d(rectified, filter, bias)
|
|
var scale = weights[scope + "/batch_normalization/gamma"]
|
|
var offset = weights[scope + "/batch_normalization/beta"]
|
|
var normalized = batchnorm(convolved, scale, offset)
|
|
// missing dropout
|
|
layers.push(normalized)
|
|
}
|
|
|
|
var layer_input = math.concat3D(layers[layers.length - 1], layers[0], 2)
|
|
var rectified = math.relu(layer_input)
|
|
var filter = weights["generator/decoder_1/conv2d_transpose/kernel"]
|
|
var bias = weights["generator/decoder_1/conv2d_transpose/bias"]
|
|
var convolved = deconv2d(rectified, filter, bias)
|
|
var rectified = math.tanh(convolved)
|
|
layers.push(rectified)
|
|
|
|
var output = layers[layers.length - 1]
|
|
var deprocessed_output = deprocess(output)
|
|
|
|
return deprocessed_output
|
|
}
|
|
|
|
|
|
// editor
|
|
|
|
function Editor(config) {
|
|
this.config = config
|
|
this.view = new View(this.config.name, 800, 400)
|
|
|
|
this.buffers = []
|
|
|
|
this.buffer = createContext(SIZE, SIZE, SCALE)
|
|
this.buffer.fillStyle = this.config.clear
|
|
this.buffer.fillRect(0, 0, SIZE, SIZE)
|
|
|
|
var image = new Image()
|
|
image.src = this.config.initial_input
|
|
image.onload = () => {
|
|
this.buffer.drawImage(image, 0, 0)
|
|
}
|
|
|
|
this.output = createContext(SIZE, SIZE, 1)
|
|
var output = new Image()
|
|
output.src = this.config.initial_output
|
|
output.onload = () => {
|
|
this.output.drawImage(output, 0, 0)
|
|
}
|
|
|
|
this.progress = null
|
|
this.last_failure = null
|
|
|
|
this.sheet_loaded = false
|
|
this.sheet = new Image()
|
|
this.sheet.src = this.config.sheet_url
|
|
this.sheet.onload = () => {
|
|
this.sheet_loaded = true
|
|
update()
|
|
}
|
|
this.sheet_index = 0
|
|
}
|
|
|
|
Editor.prototype = {
|
|
push_buffer: function() {
|
|
this.buffers.push(this.buffer)
|
|
var buffer = createContext(SIZE, SIZE, SCALE)
|
|
buffer.save()
|
|
buffer.scale(1/SCALE, 1/SCALE)
|
|
buffer.drawImage(this.buffer.canvas, 0, 0)
|
|
buffer.restore()
|
|
this.buffer = buffer
|
|
},
|
|
pop_buffer: function() {
|
|
if (this.buffers.length == 0) {
|
|
return
|
|
}
|
|
this.buffer = this.buffers.pop()
|
|
},
|
|
render: function() {
|
|
var v = this.view
|
|
|
|
v.ctx.clearRect(0, 0, v.f.width, v.f.height)
|
|
v.ctx.save()
|
|
v.ctx.scale(1/SCALE, 1/SCALE)
|
|
v.ctx.drawImage(editor_background, 0, 0)
|
|
v.ctx.restore()
|
|
|
|
v.frame("tools", 8, 41, 100, 250, () => {
|
|
var i = 0
|
|
for (var name in this.config.colors) {
|
|
var color = this.config.colors[name]
|
|
v.frame("color_selector", 0, i*21, v.f.width, 20, () => {
|
|
if (v.contains(mouse_pos)) {
|
|
cursor_style = "pointer"
|
|
}
|
|
|
|
if (mouse_released && v.contains(mouse_pos)) {
|
|
this.config.draw = color
|
|
update()
|
|
}
|
|
|
|
if (this.config.draw == color) {
|
|
v.ctx.save()
|
|
var radius = 5
|
|
v.ctx.beginPath()
|
|
v.ctx.moveTo(radius, 0)
|
|
var sides = [v.f.width, v.f.height, v.f.width, v.f.height]
|
|
for (var i = 0; i < sides.length; i++) {
|
|
var side = sides[i]
|
|
v.ctx.lineTo(side - radius, 0)
|
|
v.ctx.arcTo(side, 0, side, radius, radius)
|
|
v.ctx.translate(side, 0)
|
|
v.ctx.rotate(90 / 180 * Math.PI)
|
|
}
|
|
v.ctx.fillStyle = rgba([0.5, 0.5, 0.5, 1.0])
|
|
v.ctx.stroke()
|
|
v.ctx.restore()
|
|
v.ctx.font = "bold 8pt Arial"
|
|
} else {
|
|
v.ctx.font = "8pt Arial"
|
|
}
|
|
|
|
v.ctx.fillText(name, v.f.width - v.ctx.measureText(name).width - 26, 10)
|
|
|
|
v.frame("color", v.f.width-25, 0, 20, 20, () => {
|
|
v.ctx.beginPath()
|
|
v.ctx.fillStyle = "#666666"
|
|
v.ctx.arc(10, 10, 9, 0, 2 * Math.PI, false)
|
|
v.ctx.fill()
|
|
v.ctx.beginPath()
|
|
v.ctx.fillStyle = color
|
|
v.ctx.arc(10, 10, 8, 0, 2 * Math.PI, false)
|
|
v.ctx.fill()
|
|
})
|
|
})
|
|
i++
|
|
}
|
|
})
|
|
|
|
v.frame("output", 530, 40, 256, 256, () => {
|
|
v.ctx.drawImage(this.output.canvas, 0, 0)
|
|
})
|
|
|
|
v.frame("input", 140, 40, 256, 256+40, () => {
|
|
v.frame("image", 0, 0, 256, 256, () => {
|
|
v.ctx.drawImage(this.buffer.canvas, 0, 0, v.f.width, v.f.height)
|
|
|
|
if (v.contains(mouse_pos)) {
|
|
cursor_style = "crosshair"
|
|
if (this.config.mode == "line" && this.config.draw == "#ffffff") {
|
|
// eraser tool
|
|
cursor_style = "url(/eraser.png) 8 8, auto"
|
|
}
|
|
}
|
|
|
|
if (this.config.mode == "line") {
|
|
// this is to make undo work with lines, rather than removing only single frame line segments
|
|
var drag_from_outside = mouse_down && v.contains(mouse_pos) && !v.contains(last_mouse_pos)
|
|
var start_inside = mouse_pressed && v.contains(mouse_pos)
|
|
if (drag_from_outside || start_inside) {
|
|
this.push_buffer()
|
|
}
|
|
|
|
if (mouse_down && v.contains(mouse_pos)) {
|
|
var last = v.relative(last_mouse_pos)
|
|
var cur = v.relative(mouse_pos)
|
|
this.buffer.beginPath()
|
|
this.buffer.lineCap = "round"
|
|
this.buffer.strokeStyle = this.config.draw
|
|
if (this.config.draw == "#ffffff") {
|
|
// eraser mode
|
|
this.buffer.lineWidth = 15
|
|
} else {
|
|
this.buffer.lineWidth = 1
|
|
}
|
|
this.buffer.moveTo(last.x, last.y)
|
|
this.buffer.lineTo(cur.x, cur.y)
|
|
this.buffer.stroke()
|
|
this.buffer.closePath()
|
|
}
|
|
} else {
|
|
if (v.contains(drag_start)) {
|
|
var start = v.relative(drag_start)
|
|
var end = v.relative(mouse_pos)
|
|
var width = end.x - start.x
|
|
var height = end.y - start.y
|
|
if (mouse_down) {
|
|
v.ctx.save()
|
|
v.ctx.rect(0, 0, v.f.width, v.f.height)
|
|
v.ctx.clip()
|
|
v.ctx.fillStyle = this.config.draw
|
|
v.ctx.fillRect(start.x, start.y, width, height)
|
|
v.ctx.restore()
|
|
} else if (mouse_released) {
|
|
this.push_buffer()
|
|
this.buffer.fillStyle = this.config.draw
|
|
this.buffer.fillRect(start.x, start.y, width, height)
|
|
v.ctx.drawImage(this.buffer.canvas, 0, 0, v.f.width, v.f.height)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
v.frame("process_button", 461 - 32, 148, 32*2, 40, () => {
|
|
if (this.progress != null) {
|
|
v.ctx.font = "12px Arial"
|
|
v.ctx.fillStyle = "#000"
|
|
var s = "downloading"
|
|
v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 5)
|
|
s = "model"
|
|
v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 15)
|
|
|
|
v.frame("progress_bar", 0, 25, v.f.width, 15, () => {
|
|
v.ctx.fillStyle = "#f92672"
|
|
v.ctx.fillRect(0, 0, v.f.width * this.progress, v.f.height)
|
|
})
|
|
} else if (request_in_progress) {
|
|
do_button(v, "running")
|
|
} else {
|
|
if (do_button(v, "process")) {
|
|
if (request_in_progress) {
|
|
console.log("request already in progress")
|
|
return
|
|
}
|
|
request_in_progress = true
|
|
this.last_failure = null
|
|
|
|
this.progress = 0
|
|
progress_cb = (retrieved, total) => {
|
|
this.progress = retrieved/total
|
|
update()
|
|
}
|
|
|
|
fetch_weights(this.config.weights_url, progress_cb).then((weights) => {
|
|
this.progress = null
|
|
update()
|
|
// delay a short period of time so that UI updates before the model uses all the CPU
|
|
delay(() => {
|
|
// var g = new dl.Graph()
|
|
|
|
var convert = createContext(SIZE, SIZE, 1)
|
|
convert.drawImage(this.buffer.canvas, 0, 0, convert.canvas.width, convert.canvas.height)
|
|
var input_uint8_data = convert.getImageData(0, 0, SIZE, SIZE).data
|
|
var input_float32_data = Float32Array.from(input_uint8_data, (x) => x / 255)
|
|
|
|
console.time('render')
|
|
const math = dl.ENV.math
|
|
math.startScope()
|
|
var input_rgba = dl.Array3D.new([SIZE, SIZE, 4], input_float32_data, "float32")
|
|
var input_rgb = math.slice3D(input_rgba, [0, 0, 0], [SIZE, SIZE, 3])
|
|
|
|
var output_rgb = model(input_rgb, weights)
|
|
|
|
var alpha = dl.Array3D.ones([SIZE, SIZE, 1])
|
|
var output_rgba = math.concat3D(output_rgb, alpha, 2)
|
|
|
|
output_rgba.getValuesAsync().then((output_float32_data) => {
|
|
var output_uint8_data = Uint8ClampedArray.from(output_float32_data, (x) => x * 255)
|
|
this.output.putImageData(new ImageData(output_uint8_data, SIZE, SIZE), 0, 0)
|
|
math.endScope()
|
|
console.timeEnd('render')
|
|
request_in_progress = false
|
|
update()
|
|
})
|
|
})
|
|
}, (e) => {
|
|
this.last_failure = e
|
|
this.progress = null
|
|
request_in_progress = false
|
|
update()
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
v.frame("undo_button", 192-32, 310, 64, 40, () => {
|
|
if (do_button(v, "undo")) {
|
|
this.pop_buffer()
|
|
update()
|
|
}
|
|
})
|
|
|
|
v.frame("clear_button", 270-32, 310, 64, 40, () => {
|
|
if (do_button(v, "clear")) {
|
|
this.buffers = []
|
|
this.buffer.fillStyle = this.config.clear
|
|
this.buffer.fillRect(0, 0, SIZE, SIZE)
|
|
this.output.fillStyle = "#FFFFFF"
|
|
this.output.fillRect(0, 0, SIZE, SIZE)
|
|
}
|
|
})
|
|
|
|
if (this.sheet_loaded) {
|
|
v.frame("random_button", 347-32, 310, 64, 40, () => {
|
|
if (do_button(v, "random")) {
|
|
// pick next sheet entry
|
|
this.buffers = []
|
|
var y_offset = this.sheet_index * SIZE
|
|
this.buffer.drawImage(this.sheet, 0, y_offset, SIZE, SIZE, 0, 0, SIZE, SIZE)
|
|
this.output.drawImage(this.sheet, SIZE, y_offset, SIZE, SIZE, 0, 0, SIZE, SIZE)
|
|
this.sheet_index = (this.sheet_index + 1) % (this.sheet.height / SIZE)
|
|
update()
|
|
}
|
|
})
|
|
}
|
|
|
|
v.frame("save_button", 655-32, 310, 64, 40, () => {
|
|
if (do_button(v, "save")) {
|
|
// create a canvas to hold the part of the canvas that we wish to store
|
|
var x = 125 * SCALE
|
|
var y = 0
|
|
var width = 800 * SCALE - x
|
|
var height = 310 * SCALE - y
|
|
var convert = createContext(width, height, 1)
|
|
convert.drawImage(v.ctx.canvas, x, y, width, height, 0, 0, convert.canvas.width, convert.canvas.height)
|
|
var data_b64 = convert.canvas.toDataURL("image/png").replace(/^data:image\/png;base64,/, "")
|
|
var data = b64_to_bin(data_b64)
|
|
var blob = new Blob([data], {type: "application/octet-stream"})
|
|
var url = window.URL.createObjectURL(blob)
|
|
var a = document.createElement("a")
|
|
a.href = url
|
|
a.download = "pix2pix.png"
|
|
// use createEvent instead of .click() to work in firefox
|
|
// also can"t revoke the object url because firefox breaks
|
|
var event = document.createEvent("MouseEvents")
|
|
event.initEvent("click", true, true)
|
|
a.dispatchEvent(event)
|
|
// safari doesn"t work at all
|
|
}
|
|
})
|
|
|
|
if (this.last_failure != null) {
|
|
v.frame("server_error", 50, 350, v.f.width, 50, () => {
|
|
v.ctx.font = "20px Arial"
|
|
v.ctx.fillStyle = "red"
|
|
v.center_text(fmt("error %s", this.last_failure))
|
|
})
|
|
}
|
|
},
|
|
}
|
|
|
|
// utility
|
|
|
|
function createContext(width, height, scale) {
|
|
var canvas = document.createElement("canvas")
|
|
canvas.width = width * scale
|
|
canvas.height = height * scale
|
|
stylize(canvas, {
|
|
width: fmt("%dpx", width),
|
|
height: fmt("%dpx", height),
|
|
margin: "10px auto 10px auto",
|
|
})
|
|
var ctx = canvas.getContext("2d")
|
|
ctx.scale(scale, scale)
|
|
return ctx
|
|
}
|
|
|
|
function b64_to_bin(str) {
|
|
var binstr = atob(str)
|
|
var bin = new Uint8Array(binstr.length)
|
|
for (var i = 0; i < binstr.length; i++) {
|
|
bin[i] = binstr.charCodeAt(i)
|
|
}
|
|
return bin
|
|
}
|
|
|
|
function delay(fn) {
|
|
setTimeout(fn, 0)
|
|
}
|
|
|
|
function default_format(obj) {
|
|
if (typeof(obj) === "string") {
|
|
return obj
|
|
} else {
|
|
return JSON.stringify(obj)
|
|
}
|
|
}
|
|
|
|
function fmt() {
|
|
if (arguments.length === 0) {
|
|
return "error"
|
|
}
|
|
|
|
var format = arguments[0]
|
|
var output = ""
|
|
|
|
var arg_index = 1
|
|
var i = 0
|
|
|
|
while (i < format.length) {
|
|
var c = format[i]
|
|
i++
|
|
|
|
if (c != "%") {
|
|
output += c
|
|
continue
|
|
}
|
|
|
|
if (i === format.length) {
|
|
output += "%!(NOVERB)"
|
|
break
|
|
}
|
|
|
|
var flag = format[i]
|
|
i++
|
|
|
|
var pad_char = " "
|
|
|
|
if (flag == "0") {
|
|
pad_char = "0"
|
|
} else {
|
|
// not a flag
|
|
i--
|
|
}
|
|
|
|
var width = 0
|
|
while (format[i] >= "0" && format[i] <= "9") {
|
|
width *= 10
|
|
width += parseInt(format[i], 10)
|
|
i++
|
|
}
|
|
|
|
var f = format[i]
|
|
i++
|
|
|
|
if (f === "%") {
|
|
output += "%"
|
|
continue
|
|
}
|
|
|
|
if (arg_index === arguments.length) {
|
|
output += "%!" + f + "(MISSING)"
|
|
continue
|
|
}
|
|
|
|
var arg = arguments[arg_index]
|
|
arg_index++
|
|
|
|
var o = null
|
|
|
|
if (f === "v") {
|
|
o = default_format(arg)
|
|
} else if (f === "s" && typeof(arg) === "string") {
|
|
o = arg
|
|
} else if (f === "T") {
|
|
o = typeof(arg)
|
|
} else if (f === "d" && typeof(arg) === "number") {
|
|
o = arg.toFixed(0)
|
|
} else if (f === "f" && typeof(arg) === "number") {
|
|
o = arg.toString()
|
|
} else if (f === "x" && typeof(arg) === "number") {
|
|
o = Math.round(arg).toString(16)
|
|
} else if (f === "t" && typeof(arg) === "boolean") {
|
|
if (arg) {
|
|
o = "true"
|
|
} else {
|
|
o = "false"
|
|
}
|
|
} else {
|
|
output += "%!" + f + "(" + typeof(arg) + "=" + default_format(arg) + ")"
|
|
}
|
|
|
|
if (o !== null) {
|
|
if (o.length < width) {
|
|
output += Array(width - o.length + 1).join(pad_char)
|
|
}
|
|
output += o
|
|
}
|
|
}
|
|
|
|
if (arg_index < arguments.length) {
|
|
output += "%!(EXTRA "
|
|
while (arg_index < arguments.length) {
|
|
var arg = arguments[arg_index]
|
|
output += typeof(arg) + "=" + default_format(arg)
|
|
if (arg_index < arguments.length - 1) {
|
|
output += ", "
|
|
}
|
|
arg_index++
|
|
}
|
|
output += ")"
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
|
|
// immediate mode UI
|
|
|
|
var SCALE = 2
|
|
|
|
var updated = true
|
|
var frame_rate = 0
|
|
var now = new Date()
|
|
var last_frame = new Date()
|
|
var animations = {}
|
|
var values = {}
|
|
|
|
var cursor_style = null
|
|
var mouse_pos = [0, 0]
|
|
var last_mouse_pos = [0, 0]
|
|
var drag_start = [0, 0]
|
|
var mouse_down = false
|
|
var mouse_pressed = false
|
|
var mouse_released = false
|
|
|
|
if (DEBUG) {
|
|
var fps_elem = document.createElement("div")
|
|
stylize(fps_elem, {
|
|
width: "300px",
|
|
height: "20px",
|
|
margin: "5px",
|
|
fontFamily: "Monaco",
|
|
fontSize: "12px",
|
|
position: "absolute",
|
|
top: fmt("%dpx", 10),
|
|
right: fmt("%dpx", 10),
|
|
})
|
|
document.body.insertBefore(fps_elem, document.body.firstChild)
|
|
|
|
var status_elem = document.createElement("div")
|
|
stylize(status_elem, {
|
|
width: "10px",
|
|
height: "10px",
|
|
margin: "5px",
|
|
position: "absolute",
|
|
top: fmt("%dpx", 10),
|
|
left: fmt("%dpx", 10),
|
|
})
|
|
document.body.insertBefore(status_elem, document.body.firstChild)
|
|
}
|
|
|
|
function View(name, width, height) {
|
|
this.ctx = createContext(width, height, SCALE)
|
|
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingText/AddingText.html
|
|
this.ctx.textBaseline = "middle"
|
|
this.frames = [{name: name, offset_x: 0, offset_y: 0, width: width, height: height}]
|
|
this.f = this.frames[0]
|
|
}
|
|
|
|
View.prototype = {
|
|
push_frame: function(name, x, y, width, height) {
|
|
this.ctx.save()
|
|
this.ctx.translate(x, y)
|
|
var current = this.frames[this.frames.length - 1]
|
|
var next = {name: name, offset_x: current.offset_x + x, offset_y: current.offset_y + y, width: width, height: height}
|
|
this.frames.push(next)
|
|
this.f = next
|
|
},
|
|
pop_frame: function() {
|
|
this.ctx.restore()
|
|
this.frames.pop()
|
|
this.f = this.frames[this.frames.length - 1]
|
|
},
|
|
frame: function(name, x, y, width, height, func) {
|
|
this.push_frame(name, x, y, width, height)
|
|
func()
|
|
this.pop_frame()
|
|
},
|
|
frame_path: function() {
|
|
var parts = []
|
|
for (var i = 0; i < this.frames.length; i++) {
|
|
parts.push(this.frames[i].name)
|
|
}
|
|
return parts.join(".")
|
|
},
|
|
relative: function(pos) {
|
|
// adjust x and y relative to the top left corner of the canvas
|
|
// then adjust relative to the current frame
|
|
var rect = this.ctx.canvas.getBoundingClientRect()
|
|
return {x: pos.x - rect.left - this.f.offset_x, y: pos.y - rect.top - this.f.offset_y}
|
|
},
|
|
contains: function(pos) {
|
|
// first check that position is inside canvas container
|
|
var rect = this.ctx.canvas.getBoundingClientRect()
|
|
if (pos.x < rect.left || pos.x > rect.left + rect.width || pos.y < rect.top || pos.y > rect.top + rect.height) {
|
|
return false
|
|
}
|
|
// translate coordinates to the current frame
|
|
var rel = this.relative(pos)
|
|
return 0 < rel.x && rel.x < this.f.width && 0 < rel.y && rel.y < this.f.height
|
|
},
|
|
put_image_data: function(d, x, y) {
|
|
this.ctx.putImageData(d, (x + this.f.offset_x) * SCALE, (y + this.f.offset_y) * SCALE)
|
|
},
|
|
center_text: function(s) {
|
|
this.ctx.fillText(s, (this.f.width - this.ctx.measureText(s).width)/2, this.f.height/2)
|
|
},
|
|
}
|
|
|
|
function do_button(v, text) {
|
|
name = v.frame_path()
|
|
|
|
if (v.contains(mouse_pos)) {
|
|
cursor_style = "pointer"
|
|
}
|
|
|
|
if (request_in_progress) {
|
|
animate(name, parse_color("#aaaaaaFF"), 100)
|
|
} else if (mouse_down && v.contains(mouse_pos)) {
|
|
animate(name, parse_color("#FF0000FF"), 50)
|
|
} else {
|
|
if (v.contains(mouse_pos)) {
|
|
animate(name, parse_color("#f477a5FF"), 100)
|
|
} else {
|
|
animate(name, parse_color("#f92672FF"), 100)
|
|
}
|
|
}
|
|
|
|
v.ctx.save()
|
|
var radius = 5
|
|
v.ctx.beginPath()
|
|
v.ctx.moveTo(radius, 0)
|
|
var sides = [v.f.width, v.f.height, v.f.width, v.f.height]
|
|
for (var i = 0; i < sides.length; i++) {
|
|
var side = sides[i]
|
|
v.ctx.lineTo(side - radius, 0)
|
|
v.ctx.arcTo(side, 0, side, radius, radius)
|
|
v.ctx.translate(side, 0)
|
|
v.ctx.rotate(90 / 180 * Math.PI)
|
|
}
|
|
v.ctx.fillStyle = rgba(calculate(name))
|
|
v.ctx.fill()
|
|
v.ctx.restore()
|
|
|
|
v.ctx.font = "16px Arial"
|
|
v.ctx.fillStyle = "#f8f8f8"
|
|
v.center_text(text)
|
|
|
|
if (request_in_progress) {
|
|
return false
|
|
}
|
|
|
|
return mouse_released && v.contains(mouse_pos) && v.contains(drag_start)
|
|
}
|
|
|
|
function stylize(elem, style) {
|
|
for (var key in style) {
|
|
elem.style[key] = style[key]
|
|
}
|
|
}
|
|
|
|
function update() {
|
|
updated = true
|
|
}
|
|
|
|
function frame() {
|
|
var raf = window.requestAnimationFrame(frame)
|
|
|
|
if (!updated && Object.keys(animations).length == 0) {
|
|
if (DEBUG) {
|
|
status_elem.style.backgroundColor = "black"
|
|
}
|
|
return
|
|
}
|
|
if (DEBUG) {
|
|
status_elem.style.backgroundColor = "red"
|
|
}
|
|
|
|
now = new Date()
|
|
cursor_style = null
|
|
updated = false
|
|
|
|
try {
|
|
render()
|
|
} catch (e) {
|
|
window.cancelAnimationFrame(raf)
|
|
throw e
|
|
}
|
|
|
|
if (cursor_style == null) {
|
|
document.body.style.cursor = "default"
|
|
} else {
|
|
document.body.style.cursor = cursor_style
|
|
}
|
|
|
|
if (DEBUG) {
|
|
var decay = 0.9
|
|
var current_frame_rate = 1 / ((now - last_frame) / 1000)
|
|
frame_rate = frame_rate * decay + current_frame_rate * (1 - decay)
|
|
fps_elem.textContent = fmt("fps: %d", frame_rate)
|
|
}
|
|
|
|
last_frame = now
|
|
last_mouse_pos = mouse_pos
|
|
mouse_pressed = false
|
|
mouse_released = false
|
|
}
|
|
|
|
function array_equal(a, b) {
|
|
if (a.length != b.length) {
|
|
return false
|
|
}
|
|
|
|
for (var i = 0; i < a.length; i++) {
|
|
if (a[i] != b[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
function animate(name, end, duration) {
|
|
if (values[name] == undefined) {
|
|
// no value has been set for this element, set it immediately
|
|
values[name] = end
|
|
return
|
|
}
|
|
|
|
var v = calculate(name)
|
|
if (array_equal(v, end)) {
|
|
return
|
|
}
|
|
if (duration == 0) {
|
|
delete animations[name]
|
|
values[name] = end
|
|
return
|
|
}
|
|
var a = animations[name]
|
|
if (a != undefined && array_equal(a.end, end)) {
|
|
return
|
|
}
|
|
animations[name] = {time: now, start: v, end: end, duration: duration}
|
|
}
|
|
|
|
function calculate(name) {
|
|
if (values[name] == undefined) {
|
|
throw "calculate used before calling animate"
|
|
}
|
|
|
|
var a = animations[name]
|
|
if (a != undefined) {
|
|
// update value
|
|
var t = Math.min((now - a.time)/a.duration, 1.0)
|
|
t = t*t*t*(t*(t*6 - 15) + 10) // smootherstep
|
|
var result = []
|
|
for (var i = 0; i < a.start.length; i++) {
|
|
result[i] = a.start[i] + (a.end[i] - a.start[i]) * t
|
|
}
|
|
if (t == 1.0) {
|
|
delete animations[name]
|
|
}
|
|
values[name] = result
|
|
}
|
|
return values[name]
|
|
}
|
|
|
|
function rgba(v) {
|
|
return fmt("rgba(%d, %d, %d, %f)", v[0] * 255, v[1] * 255, v[2] * 255, v[3])
|
|
}
|
|
|
|
var parse_color = function(c) {
|
|
return [
|
|
parseInt(c.substr(1,2), 16) / 255,
|
|
parseInt(c.substr(3,2), 16) / 255,
|
|
parseInt(c.substr(5,2), 16) / 255,
|
|
parseInt(c.substr(7,2), 16) / 255,
|
|
]
|
|
}
|
|
|
|
document.addEventListener("mousemove", function(e) {
|
|
mouse_pos = {x: e.clientX, y: e.clientY}
|
|
update()
|
|
})
|
|
|
|
document.addEventListener("mousedown", function(e) {
|
|
drag_start = {x: e.clientX, y: e.clientY}
|
|
mouse_down = true
|
|
mouse_pressed = true
|
|
update()
|
|
})
|
|
|
|
document.addEventListener("mouseup", function(e) {
|
|
mouse_down = false
|
|
mouse_released = true
|
|
update()
|
|
})
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|