/** Canvas settings.
* @name Canvas
* @property {number} width horizontal width of canvas in pixels
* @property {number} height vertical height of canvas in pixels
* @property {object} context '2D' canvas rendering context
* @property {boolean} center_canvas centers the canvas in the window
* @property {boolean} flex_canvas dynamically resizes the canvas to fill the entire window
* @property {function} draw user defined function that is called every frame, with one optional time parameter corresponding to animation's relative time evolution
*/
/** Animation settings
* @name Animation
* @property {boolean} start_animation starts animation evolution when animate() is called
* @property {boolean} loop_animation loops the animation over the animation evolution time range
* @property {number} t_start start time of animation interval
* @property {number} t_end end time of animation interval
* @property {number} fps number of frames-per-second
* @property {string} animation_mode the animation mode to be used: 'rate', 'frames' 'time'
* @property {number} animation_rate amount of time incremented each frame of animation
* @property {number} animation_frames how many total frames the animation will evolve
* @property {number} animation_time absolute time of animation evolution
*/
/**Interaction settings
* @name Interaction
* @property {function} interaction user specified function that is called every frame when interaction is enabled
* @property {boolean} interaction_enabled enables interaction on the canvas if true
* @property {string | string[]} interaction_modes valid interaction modes are 'mousedownmove', 'mousehovermove', 'touchmove', 'scroll'. Input parameter must be a single string, or an array of valid interaction modes
* @property {number[]} interaction_origin array of two numbers specifying the horizontal/vertical components of the interaction origin point
* @property {number} scroll_width size of horizontal scroll in pixels
* @property {number} scroll_height size of vertical scroll in pixels
*
* @property {number} mouseX horizontal x-coordinate in pixels
* @property {number} mouseY vertical y-component in pixels
*
* @property {number} mouseX1 horizontal x-component in unit normalized range
* @property {number} mouseY1 vertical y-component in unit normalized range
*
* @property {number} scrollX horizontal x-component of scroll range in pixels
* @property {number} scrollY vertical y-component of scroll range in pixels
*
* @property {number} scrollX1 horizontal x-component of scroll range in unit normalized range
* @property {number} scrollY1 vertical y-component of scroll range in unit normalized range
* @property {boolean} pressedMouse true when mouse is pressed down
* @property {boolean} pressedTouch true when touch is pressed down
* @property {boolean} pressed true when mouse or touch is pressed down
*/
/** Recording settings
* @name Recording
* @property {boolean} record_enabled enables recording if true
* @property {number} record_max_frames number of maximum recording frames
* @property {number} record_loops number of animation loops to record
* @property {boolean} pre_loop allows one full animation loop to complete before recording starts if true
* @property {string} filename name prefix given to recorded frames, where each frame's name is indexed as 'filename_{frame_number}'
*
*/
/**Dwitter settings
* @name Dwitter
* @property {boolean} dwitter_mode creates a namespace to interpret code conventions using short function definitions from Dwitter if true
* @property {boolean} dwitter_res sets the canvas dimensions to 1920x1080 as used on Dwitter if true
* @property {boolean} dwitter_scale rescales the canvas to user specified dimensions if true
* @property {string} dwitter_code Dwitter code as a string literal
*/
const default_settings = {
// canvas settings
center_canvas : false,
flex_canvas : false,
canvas_margin : "-8px",
// Evolver settings
start_animation : true,
loop_animation : false,
t_start : 0,
t_end : undefined,
// animation settings
fps: 50,
animation_mode : "rate",
animation_rate : 0.01,
animation_frames : undefined,
animation_time : 3,
// interaction settings
interaction_enabled : false,
interaction_modes : ['mousedownmove', 'touchmove'],
interaction_origin : [0.0, 0.0],
scroll_width : 2000,
scroll_height : 2000,
// recording settings
record_enabled : false,
record_max_frames : 1000,
record_loops : 1,
pre_loop : false,
filename : 'frame_',
// dwitter settings
dwitter_mode : false,
dwitter_res : false,
dwitter_scale : false,
dwitter_code : undefined
}
function canvasIDcheck(canvas_id) {
try {
let target_element = document.getElementById(canvas_id);
if (target_element == undefined) {
if ( !(canvas_id == undefined) && typeof canvas_id != "string" || canvas_id == '') {
let msg = "Input to 'CanvasAnimater()' must be a nonempty string corresponding to the 'id' of an existing 'CANVAS' DOM element; or 'undefined' to add a new 'CANVAS' element to the DOM.";
throw msg
}
let canvas_element = document.createElement('canvas');
canvas_element.id = canvas_id;
document.body.appendChild(canvas_element);
//return new CanvasAnimater(canvas_id);
}
else if (target_element.nodeName != 'CANVAS') {
console.log(`DOM element with id=${canvas_id} is not of type <canvas>`);
}
else {
//return new CanvasAnimater(canvas_id);
}
}
catch(msg) {
console.error(msg);
alert(msg);
}
}
/** A class that animates a canvas with parameterized time evolution, interactivity, and recording.
* @param {string} canvas_id canvas DOM element's HTML 'id' attribute
*/
class CanvasAnimater {
constructor(canvas_id) {
canvasIDcheck(canvas_id);
this.animater = new Animater(canvas_id);
this.settings = this.animater.animation_settings;
}
set settings(settings_object) {
this.setAnimater(settings_object);
}
get context() {
return this.animater.canvas.context;
}
set draw(function_definition) {
this.animater.draw = function_definition;
}
get width() {
return this.animater.width;
}
set width(width) {
this.setAnimater({width : width});
}
get height() {
return this.animater.height;
}
set height(height) {
this.setAnimater({height : height});
}
set flex_canvas(flex_canvas) {
this.flex(flex_canvas);
}
set center_canvas(center_canvas) {
this.center(center_canvas);
}
set start_animation(start_animation) {
this.setAnimater({start_animation : start_animation});
}
set loop_animation(loop_animation) {
this.loop(loop_animation);
}
set t_start(t_start) {
this.setAnimater({t_start : t_start});
}
set t_end(t_end) {
this.setAnimater({t_end : t_end});
}
set fps(fps) {
this.setAnimater({fps : fps});
}
set animation_mode(animation_mode) {
this.setAnimater({animation_mode : animation_mode});
}
set animation_rate(animation_rate) {
this.setAnimater({animation_rate : animation_rate});
}
set animation_frames(animation_frames) {
this.setAnimater({animation_frames : animation_frames});
}
set animation_time(animation_time) {
this.setAnimater({animation_time : animation_time});
}
set dwitter_mode(dwitter_mode) {
this.setAnimater({dwitter_mode : dwitter_mode});
}
set dwitter_res(dwitter_res) {
this.setAnimater({dwitter_res : dwitter_res});
}
set dwitter_scale(dwitter_scale) {
this.setAnimater({dwitter_scale : dwitter_scale});
}
set dwitter_code(code_string) {
this.animater.dwitterCode_string = code_string;
this.animater.dwitterCode_set = true;
this.setAnimater({dwitter_mode : true});
}
set record_enabled(record_enabled) {
this.setAnimater({record_enabled : record_enabled});
}
set record_max_frames(record_max_frames) {
this.setAnimater({record_max_frames : record_max_frames});
}
set record_loops(record_loops) {
this.setAnimater({record_loops : record_loops});
}
set pre_loop(pre_loop) {
this.setAnimater({pre_loop : pre_loop});
}
set filename(filename) {
this.setAnimater({filename : filename});
}
set interaction(function_definition) {
this.animater.interaction = function_definition;
}
set interaction_enabled(interaction_enabled) {
this.setAnimater({interaction_enabled : interaction_enabled});
}
set interaction_modes(interaction_modes) {
this.setAnimater({interaction_modes : interaction_modes});
}
set interaction_origin(interaction_origin) {
this.setAnimater({interaction_origin : interaction_origin});
}
set scroll_width(scroll_width) {
this.setAnimater({scroll_width : scroll_width});
}
set scroll_height(scroll_height) {
this.setAnimater({scroll_height : scroll_height});
}
get mouseX() {
return this.animater.interacter.canvas_x;
}
get mouseY() {
return this.animater.interacter.canvas_y;
}
get mouseX1() {
return this.animater.interacter.norm_x;
}
get mouseY1() {
return this.animater.interacter.norm_y;
}
get scrollX() {
return this.animater.interacter.scroll_x;
}
get scrollY() {
return this.animater.interacter.scroll_y;
}
get scrollX1() {
return this.animater.interacter.scroll_norm_x;
}
get scrollY1() {
return this.animater.interacter.scroll_norm_y;
}
get pressedMouse() {
return this.animater.interacter.mouse_pressed;
}
get pressedTouch() {
return this.animater.interacter.touch_pressed;
}
get pressed() {
return this.animater.interacter.mouse_pressed || this.animater.interacter.touch_pressed;
}
/** Configures animation settings with an object containing any number of defined CanvasAnimater settings.
* @param {object} settings_object object with key/value pairs of CanvasAnimater settings
*/
setAnimater(settings_object) {
this.animater.setAnimater(settings_object);
}
/** Prints an object of configured animation settings to the console.
*/
printSettings() {
console.log(this.animater.animation_settings);
}
/** Sets how many frame-per-second the animation is rendered at.
* @param {number} fps number of frames-per-second
*/
FPS(fps) {
this.setAnimater({fps : fps});
}
/** Sets the width and height dimensions of the canvas.
* @param {number} width horizontal width of the canvas in pixels.
* @param {number} height vertical height of the canvas in pixels.
*/
size(width, height = width) {
this.setAnimater({width : width, height : height});
}
/** Centers the canvas in the window.
* @param {boolean} center_canvas canvas is centered if true
*/
center(center_canvas = true) {
this.setAnimater({center_canvas : center_canvas});
}
/** Dynamically resizes the width and height of the canvas to fill the entire window.
* @param {boolean} flex_canvas canvas is flexed if true
*/
flex(flex_canvas = true) {
this.setAnimater({flex_canvas : flex_canvas});
}
/** Creates a namespace to interpret code conventions using short function definitions from Dwitter.net.
* @param {boolean} dwitter_mode enables Dwitter mode if true
* @param {boolean} dwitter_scale rescales the canvas to user specified dimensions by transforming the canvas context.
* @param {boolean} dwitter_res sets the canvas dimensions to 1920x1080 as used on Dwitter if true
*/
dwitterMode(dwitter_mode = true, dwitter_scale = false, dwitter_res = false) {
this.setAnimater({dwitter_mode : dwitter_mode, dwitter_scale : dwitter_scale, dwitter_res : dwitter_res});
}
/** Specifies which interaction modes are enabled for the canvas.
* @param {(string|string[])} interaction_modes input parameter must be a single string, or an array of valid interaction modes: 'mousedownmove', 'mousehovermove', 'touchmove', 'scroll'.
* @param {bollean} interaction_enabled enables interaction on the canvas if true
*/
interact(interaction_modes, interaction_enabled = true) {
this.setAnimater({interaction_enabled : interaction_enabled, interaction_modes : interaction_modes});
}
/** Sets the relative coordinates for the origin point for which various interaction parameters are defined.
* @param {number} x0 horixontal x-component of the origin point
* @param {number} y0 vertical y-component of the origin point
*/
interactionOrigin(x0 = 0.0, y0 = 0.0) {
this.setAnimater({interaction_origin : [x0, y0]});
}
/** Sets the width (horizontal) and height(vertical) components of a canvas wrapper to enable scrolling parameters for canvas interaction when 'scroll' interaction is enabled.
* @param {number} width size of horizontal scroll in pixels
* @param {number} height size of vertical scroll in pixels
*/
scrollSize(width = 2000, height = 2000) {
this.setAnimater({scroll_width : width, scroll_height : height});
}
/** Sets which animation mode should be used. Possible types are 'rate', ' frames', 'time'. Additional optional parameters set values to various settings depending on which animation mode is used.
*
* 'animation_rate' is the amount of time incremented each frame of animation.
*
* 'animation_frames' is how many total frames the animation will evolve.
*
* 'animation_time' is the absolute time of animation evolution.
* @param {string} animation_mode valid strings are 'rate', 'frames', 'time'
* @param {number} p0 If animation_mode='rate', then p0= 'animation_rate'. If animation_mode='frames', then p0= 'animation_frames'. If animation_mode='time', then p0= 'animation_time'.
* @param {number} p1 If animation_mode='rate', then p1=undefined. If animation_mode='frames', then p1= 'animation_rate'. If animation_mode='time', then p0= 'animation_rate'.
*/
mode(animation_mode, p0, p1 = undefined) {
let settings = {animation_mode : animation_mode};
if (animation_mode == 'rate') {
if (!(p0 == null)) {
settings['animation_rate'] = p0;
}
}
if (animation_mode == 'frames') {
if (!(p0 == null)) {
settings['animation_frames'] = p0;
}
if (!(p1 == null)) {
settings['animation_rate'] = p1;
}
}
if (animation_mode == 'time') {
if (!(p0 == null)) {
settings['animation_time'] = p0;
}
if (!(p1 == null)) {
settings['animation_rate'] = p1;
}
}
this.setAnimater(settings);
}
/** Sets a time interval through which the animation will evolve.
*
* @param {number} t_start start time of animation interval
* @param {number} t_end end time of animation interval
*/
evolve(t_start = this.animater.animation_settings.t_start, t_end = this.animater.animation_settings.t_end) {
this.setAnimater({t_start : t_start, t_end : t_end});
}
/** Loops the animation over the animation evolution time interval.
*
* @param {boolean} loop_animation animation is looped if true
*/
loop(loop_animation = true) {
this.setAnimater({loop_animation : loop_animation});
}
/** Enables recording of the canvas by saving individual frames of the animation.
*
* @param {boolean} record_enabled enables recording if true
* @param {string} filename name prefix given to recorded frames, where each frame's name is indexed as 'filename_{frame_number}'
* @param {number} record_loops number of animation loops to record
* @param {boolean} pre_loop allows one full animation loop to complete before recording starts if true
*/
record(record_enabled = true, filename = 'frame_', record_loops = 1, pre_loop = false) {
this.setAnimater({record_enabled: record_enabled, filename : filename, record_loops : record_loops, pre_loop : pre_loop});
}
/** Sets a maximum limit to the number of frames to record.
*
* @param {number} record_max_frames number of maximum recording frames
*/
recordMaxFrames(record_max_frames) {
this.setAnimater({record_max_frames : record_max_frames});
}
/** Configures and starts the animation
*
* @param {object} settings_object an object of CanvasAnimater settings.
*/
animate(settings_object) {
this.animater.animate(settings_object);
}
}
class Animater {
constructor(canvas_id) {
this.animation_settings = default_settings;
this.draw = function() {};
// canvas settings
//this.width;
//this.height;
this.flex_canvas = false;
this.center_canvas = false;
this.canvas_margin = "-8px";
this.canvas_id = canvas_id;
this.canvas = new Canvas(this.canvas_id);
this.canvas_context = this.canvas.context;
// Evolver settings
this.loop_animation = false;
this.t_start = 0;
this.t_end;
this.t0 = this.t_start;
this.t1 = this.t_end;
this.t = 0;
this.now = 0;
this.then = 0;
this.delta = 0;
this.evolver = new Evolver(this.t0, this.t1, this.animation_rate, this.loop_animation);
// animation settings
this.fps = 50;
this.animation_mode = "rate";
this.animation_rate = 0.01;
this.animation_frames = undefined;
this.animating = false;
this.rafID = undefined,
// interaction settings
this.interaction_enabled = false;
this.interaction_modes = ['mousedownmove, touchmove'];
this.interaction_origin = [0,0];
this.scroll_width = 0,
this.scroll_height = 0,
this.interacter = new Interacter(this.canvas.element, false);
this.interaction = function() {};
// recording settings
this.record_enabled = false;
this.record_max_frames = 1000;
this.record_loops = 1;
this.pre_loop = false;
this.filename = 'frame_';
this.recorder = new Recorder(this.canvas.element, this.filename);
// dwitter settings
this.dwitter_mode = false;
this.dwitter_res = false;
this.dwitter_scale = true;
this.dwitterCode_set = false;
this.dwitterCode_string = undefined;
}
set settings(settings) {
this.setAnimater(settings);
}
get context() {
return this.canvas.context;
}
get width() {
return this.canvas.width;
}
set width(width) {
this.canvas.width = width;
}
get height() {
return this.canvas.height;
}
set height(height) {
this.canvas.height = height;
}
setAnimater(settings_object) {
let keys = Object.keys(settings_object);
keys.forEach(setting => {this[setting] = this.animation_settings[setting] = settings_object[setting]});
}
FPS(fps) {
this.setAnimater({fps : fps});
}
size(width, height) {
this.setAnimater({width : width, height : height});
}
mode(animation_mode, p0, p1 = undefined) {
let settings = {animation_mode : animation_mode};
if (animation_mode == 'rate') {
if (!(p0 == null)) {
settings['animation_rate'] = p0;
}
}
if (animation_mode == 'frames') {
if (!(p0 == null)) {
settings['animation_frames'] = p0;
}
if (!(p1 == null)) {
settings['animation_rate'] = p1;
}
}
if (animation_mode == 'time') {
if (!(p0 == null)) {
settings['animation_time'] = p0;
}
if (!(p1 == null)) {
settings['animation_rate'] = p1;
}
}
this.setAnimater(settings);
}
evolve(t_start = this.animation_settings.t_start, t_end = this.animation_settings.t_end, loop_animation = this.animation_settings.loop_animation) {
this.setAnimater({t_start : t_start, t_end : t_end, loop_animation : loop_animation});
}
flexCanvas(flex_canvas = true) {
this.setAnimater({flex_canvas : flex_canvas});
}
centerCanvas(center_canvas = true) {
this.setAnimater({center_canvas : center_canvas});
}
flex() {
this.canvas.resize(window.innerWidth, window.innerHeight);
this.width = this.canvas.width;
this.height = this.canvas.height;
window.addEventListener('resize', e => {
this.canvas.resize(window.innerWidth, window.innerHeight);
this.width = this.canvas.width;
this.height = this.canvas.height;
if (this.interaction_modes.includes("scroll")) {
this.canvas.resize_wrapper(this.scroll_width, this.scroll_height);
}
});
}
setCanvas(canvas_id) {
this.canvas.element = this.animation_settings.canvas = document.getElementById(canvas_id);
this.canvas.element.style = `margin : ${this.canvas_margin}`;
if (!(this.width == null)) {
this.canvas.width = this.canvas.element.width = this.width;
}
else {
this.width = this.canvas.width = this.canvas.element.width;
}
if (!(this.height == null)) {
this.canvas.height = this.canvas.element.height = this.height;
}
else {
this.height = this.canvas.height = this.canvas.element.height;
}
if (this.center_canvas || this.interaction_modes.includes('scroll')) {
let wrapper_width = 0;
let wrapper_height = 0;
if (this.interaction_modes.includes('scroll')) {
wrapper_width = this.scroll_width;
wrapper_height = this.scroll_height;
}
this.canvas.create_wrapper(wrapper_width, wrapper_height);
this.interacter.add_wrapper(this.canvas.wrapper, wrapper_width, wrapper_height);
}
if (this.center_canvas && !this.flex_canvas) {
this.canvas.recenter();
}
if (this.flex_canvas) {
this.flex();
}
}
setDwitterMode() {
window.c = this.canvas.element;
window.x = this.canvas.context;
window.S = Math.sin;
window.C = Math.cos;
window.T = Math.tan;
window.R =(r,g,b,a=1)=>{return `rgba(${r},${g},${b},${a})`};
if (this.dwitter_res == true) {
this.setDwitterRes(1920, 1080);
}
this.draw = this.convertDwitterDraw();
}
convertDwitterDraw() {
let code_string;
if (this.dwitterCode_set) {
code_string = this.dwitterCode_string;
}
else {
code_string = this.draw.toString()
code_string = code_string.substring(code_string.indexOf("{")+1, code_string.length-1);
}
if (this.dwitter_scale) {
code_string = "c.width|=0;x.scale(c.width/1920,c.width/1920);"+code_string;
}
return new Function('t', code_string);
}
setDwitterRes(w=1920,h=1080) {
this.canvas.element.width = w;
this.canvas.element.height = h;
}
setEvolver() {
this.t0 = this.t_start;
this.t1 = this.t_end;
let total_iterations;
if (this.loop_animation && this.t1 == null) {
//console.log("Animater setting 't_end' must be defined if 'loop_animation' is 'true'.");
throw "Animater setting 't_end' must be defined if 'loop_animation' is 'true'."
}
if (!["rate", "frames", "time"].includes(this.animation_mode)) {
//console.log("'animation_mode' must be defined as either 'rate', 'time', or 'frames'.")
throw "'animation_mode' must be defined as either 'rate', 'time', or 'frames'.";
}
if (this.animation_mode == "rate") {
if (this.animation_rate == null) {
//console.log("'animation_rate' must be defined if 'animation_mode' is 'rate'.");
throw "'animation_rate' must be defined if 'animation_mode' is 'rate'.";
}
else {
this.dt = Math.abs(this.animation_rate);
if (!(this.t1 == null)) {
total_iterations = Math.ceil(Math.abs(this.t1 - this.t0)/this.dt);
}
}
}
else if (this.animation_mode == "frames") {
if (this.animation_frames == null) {
//console.log("'animation_frames' must be defined if 'animation_mode' is 'frames'.");
throw "'animation_frames' must be defined if 'animation_mode' is 'frames'.";
}
else if (this.t1 == null) {
if (this.animation_rate == null) {
//console.log("Animater setting 'animation_rate' must be defined if 'animation_mode' is 'frames' and 't_end' is undefined.");
throw "Animater setting 'animation_rate' must be defined if 'animation_mode' is 'frames' and 't_end' is undefined." ;
}
else {
this.dt = Math.abs(this.animation_rate);
total_iterations = this.animation_frames;
}
}
else {
this.dt = Math.abs(this.t1 - this.t0)/this.animation_frames;
total_iterations = this.animation_frames;
}
}
else if (this.animation_mode == "time") {
if (this.animation_time == null) {
//console.log("Animater setting 'animation_time' must be defined if 'animation_mode' is 'time'.");
throw "Animater setting 'animation_time' must be defined if 'animation_mode' is 'time'." ;
}
else {
if (this.t1 == null) {
if (this.animation_rate == null) {
//console.log("Animater setting 'animation_rate' must be defined if 'animation_mode' is 'time' and 't_end' is undefined.");
throw "Animater setting 'animation_rate' must be defined if 'animation_mode' is 'time' and 't_end' is undefined." ;
}
else {
this.dt = Math.abs(this.animation_rate);
total_iterations = this.fps*this.animation_time;
this.t1 = this.dt*total_iterations;
}
}
else {
total_iterations = this.fps*this.animation_time;
this.dt = Math.abs(this.t1 - this.t0)/total_iterations;
}
}
}
this.evolver.set(this.t0, this.t1, this.dt, this.loop_animation, total_iterations);
}
setInteracter(canvas, interaction, mode_list = ['mousedownmove', 'touchmove'], origin=[0,0], interaction_enabled) {
this.interacter.canvas = canvas;
this.interacter.interaction_enabled = interaction_enabled;
this.interacter.interaction = interaction;
this.interacter.flex_canvas = this.flex_canvas;
this.interacter.origin = origin;
this.interacter.update_origin();
this.interaction_modes = mode_list;
this.interacter.set_modes(mode_list);
if (interaction_enabled) {
this.interacter.listen();
}
}
setRecorder(canvas, filename, record_enabled = true) {
let total_frames;
if (this.loop_animation) {
total_frames = this.animation_frames*this.record_loops;
}
else if (this.animation_mode == "rate" && this.t1 == null) {
total_frames = this.record_max_frames;
}
else {
total_frames = this.evolver.total_iterations;
}
try {
if (total_frames > this.record_max_frames) {
let msg = `Request to record ${total_frames} frames exceeds maximum: 'record_max_frames = ${this.record_max_frames}' frames will be recorded!`;
//console.log(msg);
throw msg;
}
}
catch(msg) {
alert(msg);
}
this.recorder.setRecorder(canvas, filename, total_frames);
this.record_enabled = record_enabled;
}
record() {
if ( this.recorder.frame < this.record_max_frames) {
if (this.loop_animation) {
let pre_count;
if (this.pre_loop == true) {
pre_count = 1;
}
else {
pre_count = 0;
}
if ( pre_count <= this.evolver.loop_count && this.evolver.loop_count < pre_count + this.record_loops ) {
this.recorder.record();
}
else if (this.evolver.loop_count >= pre_count + this.record_loops) {
this.record_enabled = false;
}
}
else {
if (this.recorder.frame < this.recorder.total_frames) {
this.recorder.record();
}
}
}
else {
try {
this.record_enabled = false;
let msg = `Maximum number of ${this.record_max_frames} recorded frames reached. Change setting with Animater property 'record_max_frames'.`;
//console.log(msg);
throw msg;
}
catch(msg) {
alert(msg);
this.stopAnimation(this.rafID);
}
}
}
render(t) {
this.draw(t);
if (this.record_enabled) {
this.record();
}
}
throttle(time) {
this.rafID = requestAnimationFrame(this.throttle.bind(this));
let frame_dt = 1000/this.fps
this.now = time;
this.delta = this.now - this.then;
if (this.delta > frame_dt) {
this.then = this.now - (this.delta % frame_dt);
this.render(this.t);
if (this.evolver.evolving && this.start_animation) {
this.t = this.evolver.evolve();
}
if (!(this.flex_canvas || this.interaction_enabled) ) {
if (!this.evolver.evolving || !this.start_animation) {
this.stopAnimation(this.rafID);
//console.log("Animation stopped.");
}
}
}
}
stopAnimation(requestID) {
cancelAnimationFrame(requestID);
this.animating = false;
}
animate(settings) {
try {
if (!(settings == null)) {
this.setAnimater(settings);
}
else {
this.setAnimater(this.animation_settings);
}
this.animating = this.animation_settings.start_animation;
this.setCanvas(this.canvas_id);
if (this.dwitter_mode) {
this.setDwitterMode();
}
this.setEvolver();
if (this.interaction_enabled) {
this.setInteracter(this.canvas.element, this.interaction, this.interaction_modes, this.interaction_origin,this.interaction_enabled);
}
if (this.record_enabled) {
this.setRecorder(this.canvas.element, this.filename, this.record_enabled);
}
this.then = window.performance.now();
this.throttle();
}
catch(e) {
this.stopAnimation(this.rafID);
console.log("Animater stopped.");
console.error(e)
alert(e);
}
}
}
class Canvas {
constructor(canvas_id) {
this.id = canvas_id;
this.element = document.getElementById(this.id);
this.context = this.element.getContext('2d');
//this.width = this.element.width;
//this.height = this.element.height;
this.style;
this.wrapper;
this.wrapper_width;
this.wrapper_height;
this.flex_canvas;
this.center_canvas;
}
get width() {
return this.element.width;
}
set width(width) {
this.element.width = width;
}
get height() {
return this.element.height;
}
set height(height) {
this.element.height = height;
}
resize(width, height) {
this.width = this.element.width = width;
this.height = this.element.height = height;
}
position(x_pos, y_pos) {
this.element.style.left = `${x_pos}px`;
this.element.style.top = `${y_pos}px`;
}
center() {
let x_pos = (window.innerWidth - this.element.width)/2;
let y_pos = (window.innerHeight - this.element.height)/2;
this.position(x_pos, y_pos);
}
recenter() {
this.center();
window.addEventListener('resize', e=> {this.center()});
}
resize_wrapper(width, height) {
this.wrapper.style.width = `${width}px`;
this.wrapper.style.height = `${height}px`;
}
create_wrapper(width, height) {
let wrapper_div = document.createElement('div');
this.element.style.position = "fixed";
wrapper_div.id = `${this.id}_wrapper`;
wrapper_div.style.width = `${width}px`;
wrapper_div.style.height = `${height}px`;
//wrapper_div.style.overflow = "hidden";
this.element.parentNode.insertBefore(wrapper_div, this.element);
wrapper_div.appendChild(this.element);
this.wrapper = wrapper_div;
}
}
class Evolver {
constructor(t0, t1, dt, loop_animation, total_iterations = undefined) {
this.evolving = true;
this.t0 = t0;
this.t1 = t1;
this.dt = (this.t0>=this.t1)?-Math.abs(dt):Math.abs(dt);
this.t = this.t0;
this.iterator = 0;
this.total_iterations = total_iterations;
this.loop_animation = loop_animation;
this.loop_count = 0;
}
set(t0, t1, dt, loop_animation, total_iterations = undefined) {
this.evolving = true;
this.t0 = t0;
this.t1 = t1;
this.dt = (this.t0>=this.t1)?-Math.abs(dt):Math.abs(dt);
this.t = this.t0;
this.iterator = 0;
this.total_iterations = total_iterations;
this.loop_animation = loop_animation;
this.loop_count = 0;
}
evolve() {
if (this.loop_animation) {
if (!(this.t1 == this.t0) && this.iterator < this.total_iterations) {
this.t += this.dt;
this.t = (this.t - this.t0)%(this.t1-this.t0)+this.t0;
this.iterator += 1;
}
else {
this.iterator = 0;
this.loop_count +=1;
}
}
else {
if (this.t1 == null) {
this.t += this.dt;
this.iterator += 1;
if (!(this.total_iterations == null) && this.iterator >= this.total_iterations) {
this.evolving = false;
}
}
else {
this.t += this.dt;
this.t = (this.t0<this.t1)?(this.t>=this.t1?this.t1:this.t):(this.t<=this.t1?this.t1:this.t);
this.iterator += 1;
if ( this.iterator >= this.total_iterations) {
this.evolving = false;
}
}
}
return this.t;
}
}
class Interacter {
constructor(canvas, interaction_enabled=true) {
this.canvas = canvas;
this.canvas_margin = (parseInt(this.canvas.style.margin.replace('px','')) || 0);
this.canvas_wrapper = undefined;
this.wrapper_width = 0;
this.wrapper_height = 0;
this.interaction_enabled = interaction_enabled;
this.interaction = function() { };
this.modes = ['mousedownmove','touchmove'];
this.axis_orientation = '+-';
this.mouse_pressed = false;
this.touch_pressed = false;
this.scrolling = false;
this.origin = [0,0]
this.origin_x = 0;
this.origin_y = 0;
this.norm_x = 0;
this.norm_y = 0;
this.canvas_x = 0;
this.canvas_y = 0;
this.scroll_x = 0;
this.scroll_Y = 0;
this.scroll_norm_x = 0;
this.scroll_norm_y = 0;
this.flex_canvas = false;
}
set_modes(mode_list) {
if (typeof(mode_list) == 'string') {
mode_list = [mode_list];
}
this.modes = mode_list;
}
set_origin(x,y) {
this.origin = [x,y];
}
update_origin() {
this.origin_x = this.origin[0]*this.canvas.width;
this.origin_y = this.origin[1]*this.canvas.height;
}
set_action(canvas, event) {
let boundary = canvas.getBoundingClientRect();
this.canvas_x = event.clientX - boundary.left - this.origin_x;
this.canvas_y = event.clientY - boundary.top - this.origin_y;
this.norm_x = this.canvas_x/(canvas.width - this.origin_x);
this.norm_y = this.canvas_y/(canvas.height - this.origin_y);
}
mouse_action(canvas, event) {
this.set_action(canvas, event);
this.interaction();
}
touch_action(canvas, events) {
let event = events.touches[0];
this.set_action(canvas, event);
this.interaction();
}
scroll_action() {
this.scroll_x = window.scrollX;
this.scroll_y = window.scrollY;
this.scroll_norm_x = this.scroll_x/(this.wrapper_width - this.canvas.width - this.canvas_margin);
this.scroll_norm_y = this.scroll_y/(this.wrapper_height - this.canvas.height - this.canvas_margin);
this.interaction();
}
add_wrapper(wrapper, width, height) {
this.canvas_wrapper = wrapper;
this.wrapper_width = width;
this.wrapper_height = height;
}
listen() {
if (this.flex_canvas) {
this.update_origin();
window.addEventListener('resize', e => {
this.update_origin();
});
}
if (this.modes.includes('mousedownmove') || this.modes.includes('mousehovermove')) {
if (this.modes.includes('mousedownmove')) {
this.canvas.addEventListener('mousedown', e => {
this.mouse_pressed = true;
this.mouse_action(this.canvas, e);
});
this.canvas.addEventListener('mouseup', e => {
this.mouse_pressed = false;
this.mouse_action(this.canvas, e);
});
}
this.canvas.addEventListener('mousemove', e => {
if (this.modes.includes('mousedownmove')) {
if(this.mouse_pressed) {
this.mouse_action(this.canvas, e);
}
}
else {
this.mouse_action(this.canvas, e);
}
})
}
if (this.modes.includes('touchmove')) {
this.canvas.addEventListener('touchstart', e => {
this.touch_pressed = true;
this.touch_action(this.canvas, e);
e.preventDefault();
})
this.canvas.addEventListener('touchend', e => {
this.touch_pressed = false;
e.preventDefault();
})
this.canvas.addEventListener('touchmove', e => {
this.touch_action(this.canvas, e);
e.preventDefault();
})
}
if (this.modes.includes('scroll')) {
window.addEventListener('scroll', e => {this.scroll_action()});
}
}
}
class Recorder {
constructor(canvas, filename = 'frame_', total_frames = this.animation_frames) {
this.canvas = canvas;
this.filename = filename;
this.total_frames = total_frames;
this.frame = 0;
this.loop = 0;
}
setRecorder(canvas, filename, total_frames) {
this.canvas = canvas;
this.filename = filename;
this.total_frames = total_frames;
}
download(filename) {
let dataURL = this.canvas.toDataURL();
let element = document.createElement('a');
element.setAttribute('href', dataURL);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
console.log('Downloaded ' + filename);
}
record() {
let frame_number = this.frame.toString().padStart(this.total_frames.toString().length, '0');
this.download(this.filename+frame_number+'.png', this.canvas);
this.frame = this.frame + 1;
}
}