first commit
This commit is contained in:
commit
9e4d936a96
9 changed files with 4864 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
jme-runtime.js
|
||||||
|
locales.js
|
||||||
|
code-editor.mjs
|
11
animate.js
Normal file
11
animate.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const drawing = editor.drawing;
|
||||||
|
const duration = 10;
|
||||||
|
const fps = 5;
|
||||||
|
const frames = [];
|
||||||
|
for(let frame = 0; frame < duration*fps; frame++) {
|
||||||
|
const t = frame/fps;
|
||||||
|
drawing.ctx.get_time = () => t;
|
||||||
|
drawing.ctx.draw();
|
||||||
|
frames.push(drawing.html);
|
||||||
|
}
|
||||||
|
console.log(frames.join('\n\n<<FRAME SEPARATOR>>\n\n'))
|
35
drawing.txt
Normal file
35
drawing.txt
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
let(
|
||||||
|
margin, 0.1,
|
||||||
|
eukleides("",-0.1-margin,-margin,sqrt(2)+0.1+margin,1+margin,
|
||||||
|
let(
|
||||||
|
ease, x -> if(x<0.5, 4x^3, 1 - ((2-2x)^3)/2),
|
||||||
|
frame, (from,to) -> ease(clamp((time-from)/(to-from), 0, 1)),
|
||||||
|
frame_lengths, [3,1,1,5],
|
||||||
|
frame_times, foldl((a,b) -> a+[a[-1]+b], [0], frame_lengths),
|
||||||
|
frame_number, (take(1, i -> frame_times[i]>=time, 0..len(frame_times)-1)+[len(frame_times)])[0]-1,
|
||||||
|
frames, frame(x,y) for: [x,y] of: zip(frame_times, frame_times[2..len(frame_times)]),
|
||||||
|
[a,b,c,d], rectangle(sqrt(2),1),
|
||||||
|
e, point(1,0),
|
||||||
|
f, point(1,1),
|
||||||
|
square_label, center(a..e..f..d) text("210×210") size(0.5),
|
||||||
|
strip_label, center(e..b..c..f) text("87×210") size(0.5),
|
||||||
|
[
|
||||||
|
switch(
|
||||||
|
frame_number<3,
|
||||||
|
[a..b..c..d
|
||||||
|
, (e..f) opacity(frames[0])
|
||||||
|
, [square_label, strip_label
|
||||||
|
] * opacity(frames[1])
|
||||||
|
]
|
||||||
|
, [ [(a..e..f..d), square_label] - vector(frames[3]*0.1, 0)
|
||||||
|
, (x + vector(frames[3]*0.1, 0)) for: x of: [(e..b..c..f), strip_label]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
//, point(0.7,0.7) text(jme_string(frame_times)) size(0.4)
|
||||||
|
//, point(0.7,0.5) text(jme_string(frames)) size(0.4)
|
||||||
|
//, point(0.5,0.3) text(jme_string(frame_number)) size(0.5)
|
||||||
|
//, point(0.5,0.1) text(jme_string(time)) size(0.5)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
2833
eukleides.mjs
Normal file
2833
eukleides.mjs
Normal file
File diff suppressed because it is too large
Load diff
1681
extension.js
Normal file
1681
extension.js
Normal file
File diff suppressed because it is too large
Load diff
46
index.html
Normal file
46
index.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<title>Eukleides drawing</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script type="module" src="script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Eukleides drawing</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div id="left-side">
|
||||||
|
<details id="files">
|
||||||
|
<summary>Editing <code id="current-file"></code></summary>
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
<form id="save-as-form">
|
||||||
|
<label for="save-as">Save as</label>
|
||||||
|
<input id="save-as" type="text">
|
||||||
|
<button>Save</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
<code-editor id="code"></code-editor>
|
||||||
|
</div>
|
||||||
|
<div id="right-side">
|
||||||
|
<p><a id="download-still" href="#" download>Download still</a></p>
|
||||||
|
<p>
|
||||||
|
<label for="fps">FPS:</label>
|
||||||
|
<input type="number" min="1" value="10" id="fps">
|
||||||
|
<label for="width">Width:</label>
|
||||||
|
<input type="number" min="1" value="600" id="width">
|
||||||
|
</p>
|
||||||
|
<p><button type="button" id="make-animation">Make animation</button> <a id="download-animation" download>Download animation</a></p>
|
||||||
|
<p>
|
||||||
|
<input type="range" min="0" max="10" value="0" step="0.01" id="time">
|
||||||
|
<output for="time">0</output>
|
||||||
|
</p>
|
||||||
|
<div id="display"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
make_animation.py
Normal file
30
make_animation.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
fps = 5
|
||||||
|
|
||||||
|
infile = Path(sys.argv[1])
|
||||||
|
|
||||||
|
with open(infile) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
fps = data['fps']
|
||||||
|
duration = data['duration']
|
||||||
|
frames = data['frames']
|
||||||
|
|
||||||
|
print(len(frames))
|
||||||
|
for i,frame in enumerate(frames):
|
||||||
|
print(i)
|
||||||
|
pngdir = Path('pngs')
|
||||||
|
pngdir.mkdir(exist_ok=True)
|
||||||
|
filename = Path(f'frame-{i:04d}.png')
|
||||||
|
pngname = pngdir / filename
|
||||||
|
with open(pngname, 'wb') as f:
|
||||||
|
png = base64.b64decode(frame[len('data:image/png;base64,'):])
|
||||||
|
f.write(png)
|
||||||
|
|
||||||
|
outfile = Path(infile.name).with_suffix('.mp4')
|
||||||
|
subprocess.run(['ffmpeg','-r',str(fps),'-f','image2','-i','pngs/frame-%04d.png','-vcodec','libx264','-crf','25','-pix_fmt','yuv420p','-vf','pad=ceil(iw/2)*2:ceil(ih/2)*2',outfile,'-y']);
|
195
script.js
Normal file
195
script.js
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import './jme-runtime.js';
|
||||||
|
import './locales.js';
|
||||||
|
import * as euk from './eukleides.mjs';
|
||||||
|
import './extension.js';
|
||||||
|
import './code-editor.mjs';
|
||||||
|
|
||||||
|
window.eukleides = euk;
|
||||||
|
|
||||||
|
function toBinary(string) {
|
||||||
|
const codeUnits = new Uint16Array(string.length);
|
||||||
|
for (let i = 0; i < codeUnits.length; i++) {
|
||||||
|
codeUnits[i] = string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
class Editor {
|
||||||
|
constructor() {
|
||||||
|
this.files = [];
|
||||||
|
this.scope = new Numbas.jme.Scope([Numbas.jme.builtinScope, Numbas.extensions.eukleides.scope])
|
||||||
|
this.files = JSON.parse(localStorage.getItem('files') || '{"drawing":""}');
|
||||||
|
this.code_editor = document.getElementById('code');
|
||||||
|
this.display = document.getElementById('display');
|
||||||
|
this.time_input = document.getElementById('time');
|
||||||
|
this.time_display = document.querySelector('output[for="time"]');
|
||||||
|
this.fps_input = document.getElementById('fps');
|
||||||
|
this.width_input = document.getElementById('width');
|
||||||
|
this.download_still_link = document.getElementById('download-still');
|
||||||
|
this.make_animation_button = document.getElementById('make-animation');
|
||||||
|
this.download_animation_link = document.getElementById('download-animation');
|
||||||
|
|
||||||
|
this.code_editor.addEventListener('change', e => {
|
||||||
|
const code = this.code_editor.value;
|
||||||
|
this.save_file(code);
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
|
||||||
|
this.time_input.addEventListener('input', e => {
|
||||||
|
this.redraw();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.save_as_form = document.getElementById('save-as-form');
|
||||||
|
this.save_as_form.addEventListener('submit', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.filename = this.save_as_form.elements['save-as'].value;
|
||||||
|
this.save_file(this.code_editor.value);
|
||||||
|
this.make_files_list();
|
||||||
|
})
|
||||||
|
|
||||||
|
this.download_still_link.addEventListener('click', (e) => this.download_still());
|
||||||
|
this.make_animation_button.addEventListener('click', (e) => this.make_animation());
|
||||||
|
|
||||||
|
this.load_file('drawing');
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
make_files_list() {
|
||||||
|
const ul = document.querySelector('#files ul');
|
||||||
|
ul.innerHTML = '';
|
||||||
|
for(let name of Object.keys(this.files)) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const button = document.createElement('button');
|
||||||
|
li.append(button);
|
||||||
|
if(name == this.filename) {
|
||||||
|
li.ariaCurrent = 'page';
|
||||||
|
}
|
||||||
|
button.type = 'button';
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
this.load_file(name);
|
||||||
|
});
|
||||||
|
button.textContent = name;
|
||||||
|
ul.append(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load_file(name) {
|
||||||
|
this.filename = name;
|
||||||
|
this.code_editor.value = this.files[this.filename] || '';
|
||||||
|
this.make_files_list();
|
||||||
|
document.getElementById('current-file').textContent = this.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
save_file(text) {
|
||||||
|
this.files[this.filename] = text;
|
||||||
|
localStorage.setItem('files', JSON.stringify(this.files));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const code = this.code_editor.value;
|
||||||
|
try {
|
||||||
|
const v = this.scope.evaluate(code);
|
||||||
|
const h = Numbas.jme.castToType(v, 'html');
|
||||||
|
this.drawing = h;
|
||||||
|
h.ctx.get_time = () => this.time_input.valueAsNumber;
|
||||||
|
h.ctx.draw();
|
||||||
|
this.display.innerHTML = '';
|
||||||
|
this.display.append(...h.value);
|
||||||
|
} catch(e) {
|
||||||
|
this.drawing = null;
|
||||||
|
this.display.innerHTML = e.message;
|
||||||
|
console.clear();
|
||||||
|
console.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
if(!this.drawing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.time_display.textContent = this.time_input.value;
|
||||||
|
this.drawing.ctx.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
async to_canvas() {
|
||||||
|
console.log('to canvas');
|
||||||
|
const {min_x, min_y, max_x, max_y} = this.drawing.ctx.drawer;
|
||||||
|
|
||||||
|
const vw = max_x - min_x;
|
||||||
|
const vh = max_y - min_y;
|
||||||
|
|
||||||
|
|
||||||
|
const width = this.width_input.valueAsNumber;
|
||||||
|
const height = vh/vw * width;
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
document.body.appendChild(img);
|
||||||
|
const canvas = new OffscreenCanvas(width, height);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// set it as the source of the img element
|
||||||
|
img.onload = async function() {
|
||||||
|
console.log('loaded');
|
||||||
|
// draw the image onto the canvas
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.fillRect(0,0,width,height);
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
const blob = await canvas.convertToBlob();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
reader.onloadend = function() {
|
||||||
|
const base64data = reader.result;
|
||||||
|
resolve(base64data);
|
||||||
|
img.parentElement.removeChild(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('setting src');
|
||||||
|
img.src = this.to_blob_url();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
to_blob_url() {
|
||||||
|
const content = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + this.display.innerHTML;
|
||||||
|
const blob = new Blob([content], {type: 'image/svg+xml'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
async download_still() {
|
||||||
|
this.download_still_link.download = this.filename+'.svg';
|
||||||
|
this.download_still_link.href = this.to_blob_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
async make_animation() {
|
||||||
|
this.download_animation_link.href = '';
|
||||||
|
const duration = 10;
|
||||||
|
const fps = this.fps_input.valueAsNumber;
|
||||||
|
const frames = [];
|
||||||
|
for(let i=0; i<fps*duration; i++) {
|
||||||
|
console.log('>> frame',i);
|
||||||
|
this.drawing.ctx.get_time = () => i/fps;
|
||||||
|
this.drawing.ctx.draw();
|
||||||
|
frames.push(await this.to_canvas())
|
||||||
|
}
|
||||||
|
const blob = new Blob([JSON.stringify({fps, duration, frames})], {type: 'application/json'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
this.download_animation_link.download = this.filename+'.json';
|
||||||
|
this.download_animation_link.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Numbas.queueScript('base', [], function () {});
|
||||||
|
Numbas.runImmediately(['jme'], function () {});
|
||||||
|
Numbas.queueScript('base', [], function () {});
|
||||||
|
Numbas.queueScript('demo', ['extensions/eukleides/eukleides.js'], function () {
|
||||||
|
Numbas.activateExtension('eukleides');
|
||||||
|
try {
|
||||||
|
window.editor = new Editor();
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})
|
30
style.css
Normal file
30
style.css
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"editor display" / 50svw 50svw;
|
||||||
|
;
|
||||||
|
width: 100svw;
|
||||||
|
|
||||||
|
& code-editor {
|
||||||
|
display: block;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #time {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #files {
|
||||||
|
[aria-current] button {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #display svg {
|
||||||
|
max-height: 50svh;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue