195 lines
6.1 KiB
JavaScript
195 lines
6.1 KiB
JavaScript
![]() |
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);
|
||
|
}
|
||
|
})
|