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

<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>