first commit

This commit is contained in:
Christian Lawson-Perfect 2025-02-09 20:33:33 +00:00
commit 9e4d936a96
9 changed files with 4864 additions and 0 deletions

195
script.js Normal file
View 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);
}
})