Write or draw as you like, then export to html and clear to write or draw again.
There's no way to maintain more than one file. There's no way to reimport the exported html.
MYSB7QNIZPXKWIWQBZWYIFY46HHKENB6VPYLO2ME2JSJVGRJPQ6QC
E5FYDACSQNKJG4USM52I6C4KTN3U4Z47C4TK4QYC6RF2FFCZCYCAC
F4QQIBEH24YBBOCBWZ3L7MR7AG7QNTYWQ76W5RDHKDALDKE757VQC
TSK2OXU2FTB2X44SN73Z4W4O6IDV6G6V3QU5UVTAGQY52Z7AHECAC
GTRSST7PQKKBFOJ7TR5SQJDWZU2SU4UABIEE2MS25QQ6OUPKXICAC
HXJ2I2OHQVQOASWIHEXMXBXM5EPDD2UP6NKAILV3IXIMXPOKTHBAC
KQWT45AA6OT3RTTBVIMG6SP5CI5EHQYDUQHMRGGG2XATQYAIZFGQC
KAT5YGM6WUSRNXPTU56RP423SCHOZYNGYBY53LMJD5M5EAJHY54AC
YD46MI2FYKOWUUVGYRRRSDEXYJUJKZO5RFVUBJBCNMZYVS3MXDAQC
KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC
NOMLSMKK3XOFEG74ITJRBTCZGS3XETGIFV4UJOJIPS324BC3IF7QC
5RDWSYK2YESTIEDMGOD2T7E4KCOA6DOM35ECMZT2XZT57JSCRJEQC
OBRUGSND2YN3AKWN7DC2NDFH3GIWVRAHSNUR6MMJMSAVLS6T4BGAC
ZQDQLLCLNA2GHTR32BP3CJ4J5M43BWJXELK2M6Y3LKUDMFIHB46QC
2CK5QI7WA7M4IVSACFGOJYAIDKRUTZVMMPSFWEJTUNMWTN7AX4NAC
OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC
VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC
R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC
2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC
LF7BWEG4DKQI7NMXMZC4LC2BE5PB42HK5PD6OYBNIDMAZBJASOKQC
FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC
ZTMRQZSWUL6FJRI4C4H37MR2IMV22DB6KRGEOUNYRWW5CTAVQFKAC
D4FEFHQCSILZFQ5VLWNXAIRZNUMCDNGJSM4UJ6T6FDMMIWYRYILQC
PNBKVYZ4ANUAZNQN6KEWYNDF7552ROZPNAPRJE7Q6O7ZZJMJ3S3QC
CZRMAMSBRVX26IXKHNPG6M3YSWMOZTM73X3XHAMBDSNETTFVRCUQC
KQWIMWJ5VRAXM7SFNWDSBZMQ6ZE3CZQTKZHVM5ZQCW4RHPTI64MQC
IDGP4BJZTKAD6ZO4RLAWYVN6IFCMIM76G6HJGPTE27K4D6CDBUHQC
MLG2OGU7OBWWPX5TDJQWTDTHSTM75WIMAW57546C4XLEVZQOYJ7AC
34TC5SYKYVUCVIQM3GNVYURQAMIXX64IOSJ4TYBPSRDS65QLTHWAC
EV36VCVF362E3QE22RO33TOCZRL3X7SJUDGVPL3YXISIR5LHK6JAC
VC2CU2GGRIWXIFJELD5NAELDUIRY5S5LEAFJCM2A5P3CUBYF3Z3AC
SBS2F7GRG4VYLB7DP2W6LPN5UEX6TK5DZFGCM2P4IVDUIUJRKJ7QC
File_select_bar_value = nil
File_select_bar_text = nil
-- state machine:
-- states:
-- - editing: if Export_filename_value == nil
-- - export: if Export_filename_value and not Export_filename_doublecheck
-- - doublecheck: if Export_filename_doublecheck
-- transitions:
-- editing | press 'export' button -> export
-- export | hit return ->
-- | Export_filename_value doesn't exist -> editing [after writing]
-- | Export_filename_value exists -> doublecheck
-- doublecheck | hit return -> editing [after writing]
-- doublecheck | hit some other key -> editing [cancel]
Export_filename_value = nil
Export_filename_text = nil
Export_filename_doublecheck = false
Editor_state = edit.initialize_state(Margin_top, 45 + Margin_left, App.screen.width-Margin_right, Font_height, Line_height)
Editor_state = edit.initialize_state(Menu_bar_height + Margin_top, Margin_left, App.screen.width-Margin_right, Font_height, Line_height)
run.draw_file_select_button()
if not Export_filename_value then
run.draw_buttons()
elseif Export_filename_doublecheck then
love.graphics.setColor(0,1,0)
love.graphics.print(Export_filename_value..' already exists. Overwrite? Hit enter/return to confirm, any other key to cancel', 15, 5)
end
function run.draw_file_select_button()
button(Editor_state, 'open', {x=0, y=0, w=40,h=40, color={1,0.7,1},
function run.draw_buttons()
local button_text = 'clear'
local width = to_text(button_text):getWidth()
local x=5
button(Editor_state, button_text, {x=x, y=5, w=width+10,h=Line_height, color={1,0.7,1},
icon = function(button_params)
local x,y = button_params.x, button_params.y
local w,h = button_params.w, button_params.h
love.graphics.setColor(0.4,0.4,0.7)
love.graphics.rectangle('line', x,y, w,h, 5,5)
love.graphics.setColor(0,0,0)
love.graphics.print(button_text, x+5, y)
end,
onpress1 = function()
Editor_state.lines = {{mode='text', data=''}}
Text.redraw_all(Editor_state)
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.cursor1 = {line=1, pos=1}
end,
})
x = x+width+10 + 10
button_text = 'export'
width = to_text(button_text):getWidth()
button(Editor_state, button_text, {x=x, y=5, w=width+10,h=Line_height, color={1,0.7,1},
App.screen.draw(File_select_bar_text, text_start_x,y-5)
Text.draw_cursor(Editor_state, text_start_x+App.width(File_select_bar_text),y-5)
App.screen.draw(Export_filename_text, text_start_x,y-5)
Text.draw_cursor(Editor_state, text_start_x+App.width(Export_filename_text),y-5)
if File_select_bar_value then return end
return edit.mouse_release(Editor_state, x,y, mouse_button)
if Export_filename_value then return end
edit.mouse_release(Editor_state, x,y, mouse_button)
if File_select_bar_value then
File_select_bar_value = File_select_bar_value..t
File_select_bar_text = nil
if Export_filename_doublecheck then
-- keychord_press did the work; we'll just clean up
Export_filename_value = nil
Export_filename_text = nil
Export_filename_doublecheck = false
elseif Export_filename_value then
Export_filename_value = Export_filename_value..t
Export_filename_text = nil
if File_select_bar_value then
if Export_filename_doublecheck then
if chord == 'return' then
local outfilename = love.filesystem.getSourceBaseDirectory()..'/'..Export_filename_value..'.html'
export(outfilename)
Current_flash = 'exported'
Current_flash_time = App.getTime()
end
elseif Export_filename_value then
run.switch_to_file(File_select_bar_value)
File_select_bar_value = nil
File_select_bar_text = nil
local outfilename = love.filesystem.getSourceBaseDirectory()..'/'..Export_filename_value..'.html'
if file_exists(outfilename) then
Export_filename_doublecheck = true
else
export(outfilename)
Export_filename_value = nil
Export_filename_text = nil
Current_flash = 'exported'
Current_flash_time = App.getTime()
end
local len = utf8.len(File_select_bar_value)
local byte_offset = Text.offset(File_select_bar_value, len)
File_select_bar_value = string.sub(File_select_bar_value, 1, byte_offset-1)
File_select_bar_text = nil
local len = utf8.len(Export_filename_value)
local byte_offset = Text.offset(Export_filename_value, len)
Export_filename_value = string.sub(Export_filename_value, 1, byte_offset-1)
Export_filename_text = nil
function run.switch_to_file(filename)
-- first make sure to save edits on any existing file
if Editor_state.next_save then
save_to_disk(Editor_state)
end
-- handle people hitting enter without a filename
if filename == '' then
return
end
-- clear the slate for the new file
Editor_state.filename = love.filesystem.getSourceBaseDirectory()..'/'..filename
load_from_disk(Editor_state)
Text.redraw_all(Editor_state)
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.cursor1 = {line=1, pos=1}
love.window.setTitle('lines.love - '..Editor_state.filename)
end
base64 = require('base64')
function export(filename)
local outfile = io.open(filename, 'w')
if outfile == nil then
error('failed to create "'..filename)
end
for _,line in ipairs(Editor_state.lines) do
if line.mode == 'drawing' then
local svg_contents = export_drawing(line)
outfile:write('<img src="data:image/svg+xml;base64,'..base64.encode(svg_contents)..'"/>')
else
outfile:write(line.data)
end
outfile:write('<br/>\n')
end
outfile:close()
end
function export_drawing(drawing)
local out = {}
table.insert(out, '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="128">\n')
for _,shape in ipairs(drawing.shapes) do
if shape.mode == 'freehand' then
export_freehand(shape, out)
elseif shape.mode == 'line' or shape.mode == 'manhattan' then
export_line(drawing, shape, out)
elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
export_polygon(drawing, shape, out)
elseif shape.mode == 'circle' then
export_circle(drawing, shape, out)
elseif shape.mode == 'arc' then
export_circle_arc(drawing, shape, out)
elseif shape.mode == 'deleted' then
-- ignore
else
print(shape.mode)
assert(false)
end
table.insert(out, '\n')
end
table.insert(out, '</svg>\n')
return table.concat(out)
end
function file_exists(filename)
local f = io.open(filename)
if f then
f:close()
return true
end
return false
end
function export_freehand(shape, out)
table.insert(out, ('<path stroke="black" fill="none" d="M %d %d L'):format(shape.points[1].x, shape.points[1].y))
for i=2,#shape.points do
table.insert(out, (' %d %d'):format(shape.points[i].x, shape.points[i].y))
end
table.insert(out, '"/>\n')
end
function export_line(drawing, shape, out)
local p1 = drawing.points[shape.p1]
local p2 = drawing.points[shape.p2]
table.insert(out, ('<line stroke="black" x1="%d" y1="%d" x2="%d" y2="%d"/>\n'):format(p1.x, p1.y, p2.x, p2.y))
export_point(p1, out)
export_point(p2, out)
end
function export_polygon(drawing, shape, out)
table.insert(out, '<polygon stroke="black" fill="none" points="')
for _,p in ipairs(shape.vertices) do
local vertex = drawing.points[p]
table.insert(out, ('%d,%d, '):format(vertex.x, vertex.y))
end
table.insert(out, '"/>\n')
for _,p in ipairs(shape.vertices) do
export_point(drawing.points[p], out)
end
end
function export_circle(drawing, shape, out)
local center = drawing.points[shape.center]
table.insert(out, ('<circle stroke="black" fill="none" cx="%d" cy="%d" r="%d"/>\n'):format(center.x, center.y, shape.radius))
export_point(center, out)
end
function export_circle_arc(drawing, shape, out)
local cx,cy = shape.center.x, shape.center.y
-- angle 0
local zx,zy = cx+shape.radius, cy
local sx,sy = geom.rotate(cx,cy, zx,zy, shape.start_angle)
local ex,ey = geom.rotate(cx,cy, zx,zy, shape.end_angle)
local sweep_flag = 0
if shape.start_angle < shape.end_angle then
sweep_flag = 1
end
table.insert(out, ('<path stroke="black" fill="none" d="M %d %d A %d,%d 0 0 %d %d,%d"/>'):format(sx,sy, shape.radius,shape.radius, sweep_flag, ex,ey))
export_point(shape.center, out)
end
function export_point(p, out)
table.insert(out, ('<circle cx="%d" cy="%d" r="1"/>\n'):format(p.x, p.y))
if p.name then
-- couple of adjustments:
-- * lines.love text coordinate starts at top left, but SVG starts at baseline.
--
-- * lines.love renders labels at x,y both offset by 5px on the screen.
-- While rendering SVG, however, we don't have pixels, only viewport coordinates. Seems to work well to just drop the increment.
table.insert(out, ('<text fill="black" x="%d" y="%d">%s</text>\n'):format(p.x, p.y+Drawing_text_baseline_height, p.name))
end
end
function basename(s)
return string.gsub(s, "(.*/)(.*)", "%2")
end
--[[
base64 -- v1.5.3 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
LICENSE
See end of file for license information.
--]]
local base64 = {}
function extract( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching )
decoder = decoder or DEFAULT_DECODER
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == 62 then s62 = charcode
elseif b64code == 63 then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
return base64
--[[
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
--]]
An editor for plain text where you can also seamlessly insert line drawings.
Write or draw as much as you like, but only to a single scratch space.
Designed for a tablet device, but usable anywhere.
When you're done, export to html and clear the slate.
etch.love is a compatible fork of [lines.love](http://akkartik.name/lines.html),
an editor for plain text where you can also seamlessly insert line drawings.
http://akkartik.name/lines.html
This in-progress fork tries to fix some issues on mobile devices with touch
screens and more restrictive storage models:
* File icons are hard to get a hold of, so this fork instead switches files
using a hotkey (`ctrl+w`)
* ... _(there will likely be others)_
By default, lines.love reads/writes the file `lines.txt` in a directory
specific to this app (https://love2d.org/wiki/love.filesystem.getSourceBaseDirectory).
* Undo/redo may be sluggish in large files. Large files may grow sluggish in
other ways. lines.love works well in all circumstances with files under
50KB.