commit d6aeabcc2229fb974cc48de0615b9ebb3e219fb6 Author: Sascha Nitsch Date: Wed Jul 3 03:06:02 2024 +0200 initial import diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a96dfb8 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +import globals from "globals"; +import tseslint from "typescript-eslint"; + + +export default [ + { + files: ["**/*.{ts}"], + plugins: ['@typescript-eslint/eslint-plugin', 'eslint-plugin-tsdoc'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + ecmaVersion: 'ES6', + sourceType: 'module' + }, + rules: { + 'tsdoc/syntax': 'warn' + }, + languageOptions: { + globals: globals.browser + } + }, + ...tseslint.configs.recommended, +]; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc71a4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +docs +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..31faf5e --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Presentationmaker.js +![PresentationMaker.js logo](public/img/presentationmaker.png) + +## What is +A simple(ish) tool to create presentation from XML source. +For those who fight against typical presentation tools. + +## Demo +A live demo can be found [here](https://userdata.contentnation.net/a5970e0955da4472b5f84a8dbb740273/presentation/) + +## Features +* HTML output for use in browser +* input from XML file (either single file or embedded into HTML) +* transition between pages +* HTML in elements supported +* Custom templates, you define them, combine them and use it +* powerful calculation inside description and templates thanks to [fparser](https://fparser.alexi.ch/) +* presentation can be formatted via normal CSS + +## Installation +If you just want to use it, take a look at the [public/index.html](public/index.html) file. It is a good start to use it. Additionally, you need the [public/presentationmaker.js](public/presentationmaker.js) script. +The CSS is optional, but encouraged. +It provides all the needed boilerplate to get you running. +### Development +If you want to help with development or want to see live updates if you change your xml, check out the project and use +``` +npm install +npx webpack serve +``` +and a browser pointing to localhost:8080 + +## Usage +The main presentation file is pure XML with a <presentation> root node. +Below this there are +### <defintions> +These are in the form of +```value``` +Later "name" can be used via ```${name}``` in the templates/pages +### <modules> +Similar to the definitions, a <module_name>...</module_name> +Creates a module that can be used later. +The module itself can use other previously defined modules as +```optional inner content``` +The parameter you add during use will be available to the module as ```${optional}``` +Existing internal modules are +#### calc +Calculates a formula and stores the result in a variable for later use. +Parameters: +* ```name``` the name of the variable +inner text is the formular to be used to calculate the value +#### img +Shows an image. +Parameters: +* ```src``` file name of the image +* ```x``` Left position of the image (relative to the page) +* ```y``` Top position of the image (relative to the page) +* ```width``` Image width +* ```height``` Image height +### options +Creates an effect or transformation layer to affect content that is rendered inside. +Parameters: +* ```opacity``` optional opacity value 0: fully transparent, 1: fully visible +* ```blendmode``` optional one the https://developer.mozilla.org/docs/Web/CSS/mix-blend-mode values +* ```flip``` optional flipping of the image (currently only y supported) +* ```rotate``` rotation values in X,Y,Z form +* ```y``` optional y axis to flip at +Inner XML is the content to display +#### text +Show a text in an invisible bounding box. +Parameters: +* ```x0``` left side of the bounding box. +* ```y0``` top side of the bounding box. +* ```x1``` right side of the bounding box (mutally exclusive to width) +* ```y1``` bottom side of the bounding box (mutally exclisve to height) +* ```pos``` shortcut to x0,y0,x1,y1 (mutually exclusive to above values) +* ```halign``` horizontal alignment in the bounding box, possible values: ```left```, ```center```, ```right```, defaults to ```center``` +* ```valign``` vertical alignment in the bounding box, possible value: ```top```, ```center```, ```bottom```, default to ```top``` +* ```fontsize``` optional font size in pixel +* ```color``` optional color +Inside the XML is the text to display +### page +This is the section for the actual pages to show. +In the pages, modules, variables as well as calulation can be used. +Parameters: +* ```id``` optional id of the page (useful for CSS styling) +* ```width``` page width, default to the ```width``` variable in the definitions section +* ```height``` page height, default to the ```height``` variable in the definitions section +* ```x``` X position of the page in the canvas, defaults to 0 +* ```y``` Y position of the page in the canvas, defaults to 0 +* ```z``` Z position of the page in the canvas, defaults to 0 +* ```rx``` rotation of the page on the X-Axis in °, defaults to 0 +* ```ry``` rotation of the page on the Y-Axis in °, defaults to 0 +* ```rz``` rotation of the page on the Z-Axis in °, defaults to 0 +* ```vx``` viewport position in X, defaults to ```x``` +* ```vy``` viewport position in Y, defaults to ```y``` +* ```vz``` viewport position in Z, defaults to ```z``` +* ```vwidth``` viewport width, defaults to ```width``` +* ```vheight``` viewport width, defaults to ```height``` +* ```vrx``` rotation of the vieport on the X-Axis in °, defaults to 0 +* ```vry``` rotation of the vieport on the Y-Axis in °, defaults to 0 +* ```vrz``` rotation of the vieport on the Z-Axis in °, defaults to 0 +* ```name``` name of the page (shown in drop down), defaults to page number + +## Example +For a fully featured example, take a look at [public/example.xml](public/example.xml) file. +It show multiple modules, some of them nested, many formular and some HTML inside the text. + diff --git a/dist/presentationmaker.js b/dist/presentationmaker.js new file mode 100644 index 0000000..24f460f --- /dev/null +++ b/dist/presentationmaker.js @@ -0,0 +1 @@ +(()=>{"use strict";var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{PresentationMaker:()=>P});var i=Object.defineProperty,s=(t,e,s)=>(((t,e,s)=>{e in t?i(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s})(t,"symbol"!=typeof e?e+"":e,s),s);const r={PI:Math.PI,E:Math.E,LN2:Math.LN2,LN10:Math.LN10,LOG2E:Math.LOG2E,LOG10E:Math.LOG10E,SQRT1_2:Math.SQRT1_2,SQRT2:Math.SQRT2};class n{static createOperatorExpression(t,e,i){if("^"===t)return new c(e,i);if("*"===t||"/"===t)return new l(t,e,i);if("+"===t||"-"===t)return new h(t,e,i);throw new Error(`Unknown operator: ${t}`)}evaluate(t={}){throw new Error("Empty Expression - Must be defined in child classes")}toString(){return""}}class o extends n{constructor(t){if(super(),s(this,"innerExpression"),this.innerExpression=t,!(this.innerExpression instanceof n))throw new Error("No inner expression given for bracket expression")}evaluate(t={}){return this.innerExpression.evaluate(t)}toString(){return`(${this.innerExpression.toString()})`}}class a extends n{constructor(t){if(super(),s(this,"value"),this.value=Number(t),isNaN(this.value))throw new Error("Cannot parse number: "+t)}evaluate(){return this.value}toString(){return String(this.value)}}class h extends n{constructor(t,e,i){if(super(),s(this,"operator"),s(this,"left"),s(this,"right"),!["+","-"].includes(t))throw new Error(`Operator not allowed in Plus/Minus expression: ${t}`);this.operator=t,this.left=e,this.right=i}evaluate(t={}){if("+"===this.operator)return this.left.evaluate(t)+this.right.evaluate(t);if("-"===this.operator)return this.left.evaluate(t)-this.right.evaluate(t);throw new Error("Unknown operator for PlusMinus expression")}toString(){return`${this.left.toString()} ${this.operator} ${this.right.toString()}`}}class l extends n{constructor(t,e,i){if(super(),s(this,"operator"),s(this,"left"),s(this,"right"),!["*","/"].includes(t))throw new Error(`Operator not allowed in Multiply/Division expression: ${t}`);this.operator=t,this.left=e,this.right=i}evaluate(t={}){if("*"===this.operator)return this.left.evaluate(t)*this.right.evaluate(t);if("/"===this.operator)return this.left.evaluate(t)/this.right.evaluate(t);throw new Error("Unknown operator for MultDiv expression")}toString(){return`${this.left.toString()} ${this.operator} ${this.right.toString()}`}}class c extends n{constructor(t,e){super(),s(this,"base"),s(this,"exponent"),this.base=t,this.exponent=e}evaluate(t={}){return Math.pow(this.base.evaluate(t),this.exponent.evaluate(t))}toString(){return`${this.base.toString()}^${this.exponent.toString()}`}}class u extends n{constructor(t,e,i=null){super(),s(this,"fn"),s(this,"varPath"),s(this,"argumentExpressions"),s(this,"formulaObject"),s(this,"blacklisted"),this.fn=null!=t?t:"",this.varPath=this.fn.split("."),this.argumentExpressions=e||[],this.formulaObject=i,this.blacklisted=void 0}evaluate(t={}){var e;t=t||{};const i=this.argumentExpressions.map((e=>e.evaluate(t)));try{let e=p(t,this.varPath,this.fn);if(e instanceof Function)return e.apply(this,i)}catch(t){}let s;try{s=p(null!=(e=this.formulaObject)?e:{},this.varPath,this.fn)}catch(t){}if(this.formulaObject&&s instanceof Function){if(this.isBlacklisted())throw new Error("Blacklisted function called: "+this.fn);return s.apply(this.formulaObject,i)}try{const t=p(Math,this.varPath,this.fn);if(t instanceof Function)return t.apply(this,i)}catch(t){}throw new Error("Function not found: "+this.fn)}toString(){return`${this.fn}(${this.argumentExpressions.map((t=>t.toString())).join(", ")})`}isBlacklisted(){return void 0===this.blacklisted&&(this.blacklisted=m.functionBlacklist.includes(this.formulaObject?this.formulaObject[this.fn]:null)),this.blacklisted}}function p(t,e,i){let s=t;for(let t of e){if("object"!=typeof s)throw new Error(`Cannot evaluate ${t}, property not found (from path ${i})`);if(void 0===s[t])throw new Error(`Cannot evaluate ${t}, property not found (from path ${i})`);s=s[t]}if("object"==typeof s)throw new Error("Invalid value");return s}class g extends n{constructor(t,e=null){super(),s(this,"fullPath"),s(this,"varPath"),s(this,"formulaObject"),this.formulaObject=e,this.fullPath=t,this.varPath=t.split(".")}evaluate(t={}){var e;let i;try{i=p(t,this.varPath,this.fullPath)}catch(t){}if(void 0===i&&(i=p(null!=(e=this.formulaObject)?e:{},this.varPath,this.fullPath)),"function"==typeof i||"object"==typeof i)throw new Error(`Cannot use ${this.fullPath} as value: It contains a non-numerical value.`);return Number(i)}toString(){return`${this.varPath.join(".")}`}}const f=class t{constructor(t,e={}){s(this,"formulaExpression"),s(this,"options"),s(this,"formulaStr"),s(this,"_variables"),s(this,"_memory"),this.formulaExpression=null,this.options={memoization:!1,...e},this.formulaStr="",this._variables=[],this._memory={},this.setFormula(t)}setFormula(t){return t&&(this.formulaExpression=null,this._variables=[],this._memory={},this.formulaStr=t,this.formulaExpression=this.parse(t)),this}enableMemoization(){this.options.memoization=!0}disableMemoization(){this.options.memoization=!1,this._memory={}}splitFunctionParams(t){let e=0,i="";const s=[];for(let r of t.split(""))if(","===r&&0===e)s.push(i),i="";else if("("===r)e++,i+=r;else if(")"===r){if(e--,i+=r,e<0)throw new Error("ERROR: Too many closing parentheses!")}else i+=r;if(0!==e)throw new Error("ERROR: Too many opening parentheses!");return i.length>0&&s.push(i),s}cleanupInputString(t){return t=t.replace(/\s+/g,""),Object.keys(r).forEach((e=>{t=t.replace(new RegExp(`\\b${e}\\b`,"g"),`[${e}]`)})),t}parse(t){return t=this.cleanupInputString(t),this._do_parse(t)}_do_parse(t){let e=t.length-1,i=0,s="initial",r=[],h="",l="",c=null,p=0;for(;i<=e;){switch(s){case"initial":if(h=t.charAt(i),h.match(/[0-9.]/))s="within-nr",l="",i--;else if(this.isOperator(h)){if("-"===h&&(0===r.length||this.isOperatorExpr(r[r.length-1]))){s="within-nr",l="-";break}if(i===e||this.isOperatorExpr(r[r.length-1])){s="invalid";break}r.push(n.createOperatorExpression(h,new n,new n)),s="initial"}else"("===h?(s="within-parentheses",l="",p=0):"["===h?(s="within-named-var",l=""):h.match(/[a-zA-Z]/)&&(i0&&r[r.length-1]instanceof a&&r.push(n.createOperatorExpression("*",new n,new n)),r.push(new g(h,this)),this.registerVariable(h),s="initial",l=""));break;case"within-nr":h=t.charAt(i),h.match(/[0-9.]/)?(l+=h,i===e&&(r.push(new a(l)),s="initial")):("-"===l&&(l="-1"),r.push(new a(l)),l="",s="initial",i--);break;case"within-func":if(h=t.charAt(i),h.match(/[a-zA-Z0-9_.]/))l+=h;else{if("("!==h)throw new Error("Wrong character for function at position "+i);c=l,l="",p=0,s="within-func-parentheses"}break;case"within-named-var":if(h=t.charAt(i),"]"===h)r.push(new g(l,this)),this.registerVariable(l),l="",s="initial";else{if(!h.match(/[a-zA-Z0-9_.]/))throw new Error("Character not allowed within named variable: "+h);l+=h}break;case"within-parentheses":case"within-func-parentheses":if(h=t.charAt(i),")"===h)if(p<=0){if("within-parentheses"===s)r.push(new o(this._do_parse(l)));else if("within-func-parentheses"===s){let t=this.splitFunctionParams(l).map((t=>this._do_parse(t)));r.push(new u(c,t,this)),c=null}s="initial"}else p--,l+=h;else"("===h&&p++,l+=h}i++}if("initial"!==s)throw new Error("Could not parse formula: Syntax error.");return this.buildExpressionTree(r)}buildExpressionTree(t){if(t.length<1)throw new Error("No expression given!");const e=[...t];let i=0,s=null;for(;ithis.evaluate(t)));let e=this.getExpression();if(!(e instanceof n))throw new Error("No expression set: Did you init the object with a Formula?");if(this.options.memoization){let i=this.resultFromMemory(t);return null!==i||(i=e.evaluate({...r,...t}),this.storeInMemory(t,i)),i}return e.evaluate({...r,...t})}hashValues(t){return JSON.stringify(t)}resultFromMemory(t){let e=this.hashValues(t),i=this._memory[e];return void 0!==i?i:null}storeInMemory(t,e){this._memory[this.hashValues(t)]=e}getExpression(){return this.formulaExpression}getExpressionString(){return this.formulaExpression?this.formulaExpression.toString():""}static calc(e,i=null,s={}){return i=null!=i?i:{},new t(e,s).evaluate(i)}};s(f,"Expression",n),s(f,"BracketExpression",o),s(f,"PowerExpression",c),s(f,"MultDivExpression",l),s(f,"PlusMinusExpression",h),s(f,"ValueExpression",a),s(f,"VariableExpression",g),s(f,"FunctionExpression",u),s(f,"MATH_CONSTANTS",r),s(f,"functionBlacklist",Object.getOwnPropertyNames(f.prototype).filter((t=>f.prototype[t]instanceof Function)).map((t=>f.prototype[t])));let m=f;class d{calc(t){if(""===t)return 0;try{return new m(t).evaluate({})}catch(t){}return 0}constructor(t){this.root=t}static getField(t,e){let i=t.getAttribute(e);return t.childNodes.forEach((t=>{if(1===t.nodeType){const s=t;s.localName===e&&(i=s.innerHTML)}})),i}resolveString(t){if(!t||t.indexOf("$")<0)return t;let e,i=t,s=0;for(;(e=i.indexOf("${",s))>=0;){if("\\"==i.at(e-1)){i=i.substring(0,e-1)+i.substring(e),s=e+1;continue}const t=i.indexOf("}",e);if(t<0)break;const r=i.substring(e+2,t),n=this.root.getDefinition(r);i=i.replace(i.substring(e,t+1),n)}return i}}class v extends d{clone(){const t=new E(this.root.getMain(),this.tree);return new v(this.root,t)}constructor(t,e){super(t),this.tree=e}render(t){this.tree.render(t)}}class w extends d{clone(){return new w(this.root,this.src,this.x,this.y,this.width,this.height,this.maskImage)}constructor(t,e,i,s,r,n,o){super(t),this.src=e,this.x=i,this.y=s,this.width=r,this.height=n,this.maskImage=o}static fromElem(t,e){return new w(t,this.getField(e,"src"),this.getField(e,"x"),this.getField(e,"y"),this.getField(e,"width"),this.getField(e,"height"),this.getField(e,"mask-image"))}render(t){const e=document.createElement("img");e.style.position="absolute",e.style.top=this.calc(this.resolveString(this.y))+"px",e.style.left=this.calc(this.resolveString(this.x))+"px",e.setAttribute("width",this.calc(this.resolveString(this.width)).toString()),e.setAttribute("height",this.calc(this.resolveString(this.height)).toString()),e.setAttribute("src",this.resolveString(this.src)),this.maskImage&&(e.style.maskImage=this.maskImage),t.append(e)}}class y extends d{clone(){return new y(this.root,this.target,this.expression)}constructor(t,e,i){super(t),this.target=e,this.expression=i}static fromElem(t,e){return new y(t,e.getAttribute("name")||"",e.innerHTML)}render(t){const e=this.resolveString(this.expression),i=new m(e).evaluate({});this.root.set(this.target,i.toString())}}class b extends d{clone(){return new b(this.root,this.x0,this.y0,this.x1,this.y1,this.width,this.height,this.pos,this.halign,this.valign,this.fontSize,this.text,this.color)}static fromElem(t,e){return new b(t,e.getAttribute("x0"),e.getAttribute("y0"),e.getAttribute("x1"),e.getAttribute("y1"),e.getAttribute("w"),e.getAttribute("h"),e.getAttribute("pos"),e.getAttribute("halign")||"center",e.getAttribute("valign"),e.getAttribute("size"),e.innerHTML,e.getAttribute("color")||t.getDefinition("defaultfontcolor"))}constructor(t,e,i,s,r,n,o,a,h,l,c,u,p){super(t),this.x0=e,this.y0=i,this.x1=s,this.y1=r,this.width=n,this.height=o,this.pos=a,this.halign=h,this.valign=l,this.fontSize=c,this.text=u,this.color=p}render(t){let e,i,s,r;if(this.pos){const t=this.resolveString(this.pos).split(",");if(4!=t.length)return;e=this.calc(this.resolveString(t[0])),i=this.calc(this.resolveString(t[1])),s=this.calc(this.resolveString(t[2])),r=this.calc(this.resolveString(t[3]))}else e=this.calc(this.resolveString(this.x0)),i=this.calc(this.resolveString(this.y0)),this.x1?(s=this.calc(this.resolveString(this.x1)),r=this.calc(this.resolveString(this.y1))):(s=e+this.calc(this.resolveString(this.width)),r=i+this.calc(this.resolveString(this.height)));const n=this.resolveString(this.halign),o=this.resolveString(this.valign),a=this.resolveString(this.text),h=document.createElement("div");switch(this.color&&(h.style.color=this.resolveString(this.color)),h.style.position="absolute",this.fontSize&&(h.style.fontSize=this.calc(this.resolveString(this.fontSize))+"px"),h.style.left=e+"px",h.style.top=i+"px",h.style.width=s-e+"px",h.style.height=r-i+"px",h.style.overflow="clip",n){case"center":h.style.textAlign="center";break;case"right":h.style.textAlign="right"}switch(o){case"center":h.style.alignContent="center";break;case"bottom":h.style.alignContent="end"}h.insertAdjacentHTML("afterbegin",a),t.append(h)}}class x extends d{clone(){return new x(this.root,this.subtree,this.opacity,this.blendmode,this.flip,this.rotate,this.y)}constructor(t,e,i,s,r,n,o){super(t),this.subtree=e,this.opacity=i,this.blendmode=s,this.flip=r,this.rotate=n,this.y=o}static fromElem(t,e,i){const s=new E(t);return s.createFromElem(i),new x(e,s,this.getField(i,"opacity")||"",this.getField(i,"blend")||"",this.getField(i,"flip")||"",this.getField(i,"rotate")||"",this.getField(i,"y")||"")}render(t){const e=document.createElement("div");if(e.classList.add("option"),""!==this.opacity&&(e.style.opacity=this.opacity),""!==this.blendmode&&(e.style.mixBlendMode=this.blendmode),""!==this.flip&&"v"==this.flip&&(e.style.transform="scaleY(-1)"),this.y&&(e.style.position="absolute",e.style.bottom="0px"),""!==this.rotate){const t=this.rotate.split(",");let i="";t[0]&&(i+="rotateX("+this.calc(this.resolveString(t[0]))+"deg)"),t[1]&&(i+="rotateY("+this.calc(this.resolveString(t[1]))+"deg)"),t[2]&&(i+="rotateZ("+this.calc(this.resolveString(t[2]))+"deg)"),e.style.transform=i,e.style.transformOrigin="top left"}this.subtree.render(e),t.append(e)}}class S extends d{clone(){return new S(this.root,this.subtree,this.id,this.transformation.size.w,this.transformation.size.h,this.transformation.position.x,this.transformation.position.y,this.transformation.position.z,this.transformation.rotation.y,this.transformation.rotation.y,this.transformation.rotation.z,this.transformation.viewpos.x,this.transformation.viewpos.y,this.transformation.viewpos.z,this.transformation.viewSize.w,this.transformation.viewSize.h,this.transformation.viewRotation.x,this.transformation.viewRotation.y,this.transformation.viewRotation.z,this.name)}constructor(t,e,i,s,r,n,o,a,h,l,c,u,p,g,f,m,d,v,w,y){super(t),this.subtree=e,this.id=i,this.transformation={size:{w:s,h:r},position:{x:n,y:o,z:a},rotation:{x:h,y:l,z:c},viewpos:{x:u,y:p,z:g},viewSize:{w:f,h:m},viewRotation:{x:d,y:v,z:w}},this.name=y}static fromElem(t,e,i){const s=new E(t);return s.createFromElem(i),new S(e,s,i.getAttribute("id")||"",i.getAttribute("width")||"${width}",i.getAttribute("height")||"${height}",i.getAttribute("x")||"0",i.getAttribute("y")||"0",i.getAttribute("z")||"0",i.getAttribute("rx")||"0",i.getAttribute("ry")||"0",i.getAttribute("rz")||"0",i.getAttribute("vx")||"",i.getAttribute("vy")||"",i.getAttribute("vz")||"",i.getAttribute("vwidth")||"",i.getAttribute("vheight")||"",i.getAttribute("vrx")||"",i.getAttribute("vry")||"",i.getAttribute("vrz")||"",i.getAttribute("name")||"${page}")}getName(){return this.name}getView(){let t,e,i;return t=""!==this.transformation.viewpos.x||""!==this.transformation.viewpos.y||""!==this.transformation.viewpos.z?{x:this.calc(this.resolveString(this.transformation.viewpos.x)),y:this.calc(this.resolveString(this.transformation.viewpos.y)),z:this.calc(this.resolveString(this.transformation.viewpos.z))}:{x:this.calc(this.resolveString(this.transformation.position.x)),y:this.calc(this.resolveString(this.transformation.position.y)),z:this.calc(this.resolveString(this.transformation.position.z))},e=""!==this.transformation.viewSize.w||""!==this.transformation.viewSize.h?{w:this.calc(this.resolveString(this.transformation.viewSize.w)),h:this.calc(this.resolveString(this.transformation.viewSize.h))}:{w:this.calc(this.resolveString(this.transformation.size.w)),h:this.calc(this.resolveString(this.transformation.size.h))},i=""!==this.transformation.viewRotation.x||""!==this.transformation.viewRotation.y||""!==this.transformation.viewRotation.z?{x:this.calc(this.resolveString(this.transformation.viewRotation.x)),y:this.calc(this.resolveString(this.transformation.viewRotation.y)),z:this.calc(this.resolveString(this.transformation.viewRotation.z))}:{x:0,y:0,z:0},{position:t,size:e,viewRotation:i}}render(t){this.root.set("page",(parseInt(this.root.getDefinition("page"))+1).toString());const e=document.createElement("article");e.classList.add("page"),e.classList.add("inactive");let i="";const s=this.calc(this.resolveString(this.transformation.position.x)),r=this.calc(this.resolveString(this.transformation.position.y)),n=this.calc(this.resolveString(this.transformation.position.z));(s||r||n)&&(i+="translate3d("+s+"px, "+r+"px, "+n+"px)");const o=-this.calc(this.resolveString(this.transformation.rotation.x)),a=-this.calc(this.resolveString(this.transformation.rotation.y)),h=-this.calc(this.resolveString(this.transformation.rotation.z));o&&(i+="rotateX("+o+"deg)"),a&&(i+="rotateY("+a+"deg)"),h&&(i+="rotateZ("+h+"deg)"),""!==i&&(e.style.transform=i);const l=this.calc(this.resolveString(this.transformation.size.w)).toString(),c=this.calc(this.resolveString(this.transformation.size.h)).toString();e.style.width=l+"px",e.style.height=c+"px",this.id&&(e.id=this.id),t.append(e),this.root.push(),this.subtree.set("width",l),this.subtree.set("height",c),this.subtree.render(e),this.root.pop()}}class E{constructor(t,e=null){this.main=t,this.elements=[],this.definitions=new Map,null!==e&&e.elements.forEach((t=>{this.elements.push(t.clone())}))}createFromElem(t){t.childNodes.forEach((t=>{if(1===t.nodeType){const e=this.createNodeFromElem(t);null!==e&&this.elements.push(e)}}))}createNodeFromElem(t){const e=t.localName;switch(e){case"calc":return y.fromElem(this,t);case"img":return w.fromElem(this,t);case"options":return x.fromElem(this.main,this,t);case"page":return S.fromElem(this.main,this,t);case"text":return b.fromElem(this,t)}const i=this.main.getNewModule(e);if(null===i)return console.log("unknown module",e),null;i.definitions.clear();for(let e=0;e{if(1===t.nodeType){const e=t;i.definitions.set(e.localName,e.innerHTML)}})),new v(this,i)}getDefinition(t){return this.main.getDefinition(t)}getElement(t){return t>=0&&t<=this.elements.length?this.elements[t]:null}getElementCount(){return this.elements.length}getMain(){return this.main}pop(){this.main.pop()}push(){this.main.push()}render(t){this.main.push(),this.definitions.forEach(((t,e)=>{this.main.set(e,t)})),this.elements.forEach((e=>{e.render(t)})),this.main.pop()}set(t,e){this.main.set(t,e)}}class P{changeSlide(t){if(t<0&&t>=this.numPages)return;this.tree.getElement(t);const e=this.viewportElement.querySelectorAll(".page");this.currentPage>=0&&this.currentPage=0&&this.currentPage=0&&this.currentPage=this.numPages?s.classList.add("disabled"):s.classList.remove("disabled")}constructor(t){if(this.handleClick=t=>{const e=t.target;let i,s=e.getAttribute("data-action");switch(null===s&&(s=e.getAttribute("data-id")),s){case"fullscreen":document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen(),t.stopPropagation(),t.preventDefault();break;case"prev":i=Math.max(this.currentPage-1,0);break;case"next":case null:i=Math.min(this.currentPage+1,this.numPages-1)}return void 0!==i&&this.changeSlide(i),e.closest(".ui")&&(t.stopPropagation(),t.preventDefault()),!1},this.handleDropdown=t=>(t.stopPropagation(),t.preventDefault(),this.changeSlide(parseInt(this.select.value)),!1),this.handleKey=t=>{switch(t.key){case"ArrowLeft":this.changeSlide(Math.max(this.currentPage-1,0));break;case"ArrowRight":this.changeSlide(Math.min(this.currentPage+1,this.numPages-1))}},this.handleMouseMove=()=>{this.timer&&window.clearTimeout(this.timer),this.ui.classList.add("visible"),document.body.style.cursor="auto",this.timer=window.setTimeout(this.hideUI,2e3)},this.handleTouchEnd=t=>{const e=t.changedTouches[0].screenX;if(Math.abs(this.touchStartX-e)>100){let t;t=this.touchStartX{this.touchStartX=t.changedTouches[0].screenX},this.hideUI=()=>{this.ui.classList.remove("visible"),document.body.style.cursor="none"},this.moveViewpoint=()=>{const t=this.tree.getElement(this.currentPage).getView();let e="";t.viewRotation.x&&(e+=" rotateX("+-t.viewRotation.x+"deg) "+e),t.viewRotation.y&&(e+=" rotateY("+-t.viewRotation.y+"deg) "+e),t.viewRotation.z&&(e+=" rotateZ("+-t.viewRotation.z+"deg) "+e),e+=" translate3d("+-t.position.x+"px,"+-t.position.y+"px, "+-t.position.z+"px)";const i=parseInt(this.getDefinition("width"))/t.size.w,s=parseInt(this.getDefinition("height"))/t.size.h,r=Math.min(i,s),n=this.viewportElement.parentElement.clientHeight/parseFloat(this.getDefinition("height"))*r;1!=n&&(e="scale("+n+")"+e),this.viewportElement.style.transform=e},void 0===t)throw"please supply a configuration";this.definitions=[],this.modules=new Map,this.currentPage=0,this.numPages=0,this.transition=t.transitionType||"flip",this.tree=new E(this,null),this.ui=document.querySelector(t.uiSelector||".ui"),this.select=this.ui.querySelector("select[data-id=pages]"),document.location.hash&&(this.currentPage=parseInt(document.location.hash.substring(1))-1),this.config=t}createDefinitions(t){const e=new Map;t.childNodes.forEach((t=>{if(1==t.nodeType){const i=t;e.set(i.localName,i.innerHTML)}})),this.definitions.push(e)}createModules(t){t.childNodes.forEach((t=>{if(1==t.nodeType){const e=t,i=new E(this);i.createFromElem(e),this.modules.set(e.localName,i)}}))}createPages(t){const e=t;this.tree.createFromElem(e),this.numPages=this.tree.getElementCount(),this.definitions[0].set("numpages",this.numPages.toString()),this.definitions[0].set("page","0");for(let t=0;t{const e=t;switch(e.localName){case"definitions":this.createDefinitions(e);break;case"modules":this.createModules(e);break;case"pages":this.createPages(e)}t=t.nextSibling})),this.viewportElement=document.createElement("div"),this.viewportElement.id="viewport",this.tree.render(this.viewportElement),document.querySelector(this.config.targetSelector).append(this.viewportElement)},new((i=void 0)||(i=Promise))((function(r,n){function o(t){try{h(s.next(t))}catch(t){n(t)}}function a(t){try{h(s.throw(t))}catch(t){n(t)}}function h(t){var e;t.done?r(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(o,a)}h((s=s.apply(t,e||[])).next())}));var t,e,i,s}getDefinition(t){let e="";return this.definitions.forEach((i=>{const s=i.get(t);void 0!==s&&(e=s)})),""===e&&console.log("failed to get definition for",t),e}getNewModule(t){const e=this.modules.get(t);return void 0!==e?new E(this,e):(console.log("unknown render element",t),null)}pop(){this.definitions.pop()}present(){window.addEventListener("resize",this.moveViewpoint),document.querySelectorAll("body, [data-action]").forEach((t=>{t.addEventListener("click",this.handleClick)})),window.addEventListener("keydown",this.handleKey),this.select.addEventListener("change",this.handleDropdown),window.addEventListener("mousemove",this.handleMouseMove),document.body.addEventListener("touchstart",this.handleTouchStart),document.body.addEventListener("touchend",this.handleTouchEnd),this.moveViewpoint(),this.changeSlide(this.currentPage)}push(){this.definitions.push(new Map)}set(t,e){this.definitions.at(-1).set(t,e)}}var M=self;for(var z in e)M[z]=e[z];e.__esModule&&Object.defineProperty(M,"__esModule",{value:!0})})(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..518ca3e --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "presentationmake.js", + "version": "0.0.1", + "description": "A tool to create browser based presentations", + "author": "Sascha Nitsch (grumpydeveloper https://contentnation.net/@grumpydevelop)", + "license": "GPL-3.0-or-later", + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "eslint": "^8.56.0", + "eslint-plugin-tsdoc": "^0.3.0", + "globals": "^15.6.0", + "ts-loader": "^9.5.1", + "typedoc": "^0.26.3", + "typescript": "^5.5.2", + "typescript-eslint": "^7.14.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + }, + "dependencies": { + "fparser": "^3.0.1" + } +} diff --git a/public/example.xml b/public/example.xml new file mode 100644 index 0000000..b48ece6 --- /dev/null +++ b/public/example.xml @@ -0,0 +1,143 @@ + + + 1920 + 1080 + #ffffff + img/ + 1720 + 165 + 1590 + 175 + 880 + 0,35*${scale},1920*${scale},270*${scale} + 90 + 105 + 235 + 445 + 655 + 72 + transform + 1 + + + + + + Introduction to presentationmaker.js + ${page}/${numpages} + + + + + + + + + + ${headertext} + ${text1} + ${text2} + ${text3} + + + + + ${headertext} + ${text1} + ${text2} + ${text3} + + + + ((${mirrorline} - ${y}) +${mirrorline} - ${h}) + + + + + + + + + + + Welcome to the presentationmaker.js example + A quick presentation of the why and how. + + + + Why using presentationmaker.js + Powerpoint / Impress are hard to use. + I wanted to concentrate on the content, not the quirks of the tools. + I want to make use of modern browser features. + + + + 2 + + + + + + + + + + + + + + Example + +This page is made by
+

+<page x="2020*3" y="1180" name="Example">
+  <defaultbg />
+  <text pos="\${headerpos}" size="\${headersize}" halign="center"
+    valign="center">Example</text>
+  <text pos="100,270,1820,860" size="40" halign="left"
+    valign="top">
+    This page is made by<br />
+    <pre><code>Recursion is nice</code>/</pre>
+  </text>
+</page>
+
+
+
+ + + + + + + + sin(30*PI/180) + sqrt(9) + + + + + + The mirrored image is an example where calc helps. + Also it uses some image effects. + + + + + + + + + + git.contentnation.net/presentationmaker + + + + + impress.js.org
(inspired, no code was used from the project.)
+ Built by GrumpyDeveloper +
+
+ + The end + +
+
diff --git a/public/img/bg.jpg b/public/img/bg.jpg new file mode 100644 index 0000000..9e35bc4 Binary files /dev/null and b/public/img/bg.jpg differ diff --git a/public/img/box.png b/public/img/box.png new file mode 100644 index 0000000..05d4ecf Binary files /dev/null and b/public/img/box.png differ diff --git a/public/img/contentnation.svg b/public/img/contentnation.svg new file mode 100644 index 0000000..3fdd315 --- /dev/null +++ b/public/img/contentnation.svg @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logo.svg b/public/img/logo.svg new file mode 100644 index 0000000..4f4121b --- /dev/null +++ b/public/img/logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/public/img/presentationmaker.png b/public/img/presentationmaker.png new file mode 100644 index 0000000..1fcfa0e Binary files /dev/null and b/public/img/presentationmaker.png differ diff --git a/public/img/presentationmaker.svg b/public/img/presentationmaker.svg new file mode 100644 index 0000000..2f42ff6 --- /dev/null +++ b/public/img/presentationmaker.svg @@ -0,0 +1,423 @@ + +PresentationMaker.jsby diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..deb348f --- /dev/null +++ b/public/index.html @@ -0,0 +1,34 @@ + + + + Example + + + + +
+
+
+ + 🠈 + + 🠊 +
+
+
+ + + + diff --git a/public/roboto.woff2 b/public/roboto.woff2 new file mode 100644 index 0000000..1a53701 Binary files /dev/null and b/public/roboto.woff2 differ diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..18ac0e2 --- /dev/null +++ b/public/style.css @@ -0,0 +1,106 @@ +@font-face { + font-family:'Roboto'; + font-style:bold; + font-weight:600; + src:url(roboto.woff2) format('woff2'); + font-display:swap +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: #000000; + font-size: min(1vw, 1.778vh); + overflow:clip; + font-family: Roboto, sans-serif; +} + +#ar { + width: 100em; + height: 56.25em; +} + +#render { + font-size: 1em; + a { + color: #BBBBff; + img { + vertical-align: bottom; + } + } + .ui { + position: fixed; + z-index: 999999; + color: #ffffff; + top: 20px; + left: 20px; + text-shadow: #000000 2px 0 0; + opacity: 0; + transition: opacity 1s ease-in-out; + font-size:2em; + &.visible { + opacity: 0.75; + } + a { + color: inherit; + text-decoration: none; + font-size: 1.0em; + } + a.disabled { + color: transparent; + cursor: default; + } + select { + margin: 0 5px; + outline: none; + } + } +} + +#fullscreen { + font-weight: bold; +} +#fs { + margin-left:10px; +} + +.page { + position: absolute; + transform-origin: top left 0px; + transition: opacity .5s ease-in-out; +} +.page.inactive { + opacity: 0.5; + transition: opacity .5s ease-in-out; +} + +[data-action] { + height: 2em; + vertical-align: middle; + cursor: pointer; +} + +#viewport { + transition: all 1s ease-in-out; + transform-style: preserve-3d; + transform-origin: top left 0px; +} +#render { + overflow: clip; + height:100vh; + perspective: 1000px; +} + +.option { + width:100%; + height:100%; +} diff --git a/src/presentationmaker.ts b/src/presentationmaker.ts new file mode 100644 index 0000000..255105c --- /dev/null +++ b/src/presentationmaker.ts @@ -0,0 +1,432 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + **/ + +import { RenderTree } from './rendertree/rendertree'; +import { RTPage, RTTransformation } from './rendertree/rtpage'; + +/** + * configuration options for PresentationMaker + */ +export interface initialConfig { + /** selector for source tree inside a <script type="presentationmaker">...</script> */ + sourceSelector?: string, + /** alternative URL instead of script tag */ + sourceURI?: string, + /** position to attach generated tree to */ + targetSelector: string, + /** optional transition type, defaults to flip */ + transitionType?: string + /** option ui selector string */ + uiSelector?: string; +} + +/** + * main class + */ +export class PresentationMaker { + /** generated render tree */ + private tree: RenderTree; + /** definitions for variable replacements */ + private definitions: Array>; + /** custom modules defined by the import document */ + private modules: Map; + /** current page index */ + private currentPage: number; + /** number of slide */ + private numPages: number; + /** our viewport (window to the presentation) */ + private viewportElement: HTMLDivElement; + /** transition type [flip / transition] */ + private transition: string; + /** time for autohiding of UI */ + private timer: number; + /** ui root node */ + private ui: HTMLDivElement; + /** page selector */ + private select: HTMLSelectElement; + /** X start position to detect slide left/right */ + private touchStartX: number; + /** initial config from caller */ + private config: initialConfig; + + /** + * change slide to given number + * @param slideNum slide number 0-based + */ + private changeSlide(slideNum: number) { + if (slideNum < 0 && slideNum >= this.numPages) { + return; + } + const newPage = this.tree.getElement(slideNum); + const page = newPage; + const pages = this.viewportElement.querySelectorAll('.page'); + if (this.currentPage >= 0 && this.currentPage < this.numPages) { + (pages[this.currentPage]).classList.add('inactive'); + } + switch (this.transition) { + case 'transform': + // this.transform = page.getView(); + this.currentPage = slideNum; + this.moveViewpoint(); + break; + default: + if (this.currentPage >= 0 && this.currentPage < this.numPages) { + (pages[this.currentPage]).style.display = 'none'; + } + this.currentPage = slideNum; + const newPage = pages[this.currentPage]; + newPage.style.display = 'block' + break; + } + if (this.currentPage >= 0 && this.currentPage < this.numPages) { + (pages[this.currentPage]).classList.remove('inactive'); + } + document.location.hash = (this.currentPage + 1).toString(); + this.select.value = this.currentPage.toString(); + const prev = document.querySelector('[data-action="prev"]'); + if (this.currentPage == 0) { + prev.classList.add('disabled'); + } else { + prev.classList.remove('disabled'); + } + const next = document.querySelector('[data-action="next"]'); + if (this.currentPage + 1 >= this.numPages) { + next.classList.add('disabled'); + } else { + next.classList.remove('disabled'); + } + } + + /** + * constructor + */ + constructor(config: initialConfig) { + if (config === undefined) { + throw('please supply a configuration'); + } + this.definitions = []; + this.modules = new Map(); + this.currentPage = 0; + this.numPages = 0; + this.transition = config.transitionType || 'flip'; + this.tree = new RenderTree(this, null); + this.ui = document.querySelector(config.uiSelector || '.ui'); + this.select = this.ui.querySelector('select[data-id=pages]'); + if (document.location.hash) { + this.currentPage = parseInt(document.location.hash.substring(1)) - 1; + } + this.config = config; + } + + /** + * create definitions from element tree + * @param elem <definition>...</definition> node tree + */ + private createDefinitions(elem: HTMLElement): void { + const definitions: Map = new Map(); + elem.childNodes.forEach((def) => { + if (def.nodeType == 1) { + const definitionElement = def; + definitions.set(definitionElement.localName, definitionElement.innerHTML); + } + }); + this.definitions.push(definitions); + + } + + /** + * create a custom module from input + * @param elem <module>...</module> node tree + */ + private createModules(elem: HTMLElement): void { + elem.childNodes.forEach((module) => { + if (module.nodeType == 1) { + const moduleElement = module; + const tree = new RenderTree(this); + tree.createFromElem(moduleElement); + this.modules.set(moduleElement.localName, tree); + } + }); + } + + /** + * create pages from input + * @param elem <slides>...</slides> node tree + */ + private createPages(elem: HTMLElement): void { + const pageElement = elem; + this.tree.createFromElem(pageElement); + this.numPages = this.tree.getElementCount(); + this.definitions[0].set('numpages', this.numPages.toString()); + this.definitions[0].set('page', '0'); + for (let i = 0; i < this.numPages; ++i) { + const newPage = this.tree.getElement(i); + const page = newPage; + const option = document.createElement('option'); + option.value = i.toString(); + option.innerText = page.getName(); + this.select.append(option); + } + } + + /** + * generate HTML tree from input XML + */ + async generate(): Promise { + let root; + const parser = new DOMParser(); + if (this.config.sourceSelector !== undefined) { + const source = document.querySelector(this.config.sourceSelector); + root = parser.parseFromString(source.outerHTML, 'application/xml').querySelector('script'); + } else { + const response = await fetch(this.config.sourceURI); + const data = await response.blob(); + root = parser.parseFromString(await data.text(), 'application/xml').querySelector('presentation'); + } + + // iterate over the elements + root.childNodes.forEach((node) => { + const elem = node; + switch (elem.localName) { + case 'definitions': + this.createDefinitions(elem); + break; + case 'modules': + this.createModules(elem); + break; + case 'pages': + this.createPages(elem); + break; + } + node = node.nextSibling; + }); + // render pages + this.viewportElement = document.createElement('div'); + this.viewportElement.id = 'viewport'; + this.tree.render(this.viewportElement); + const renderTarget = document.querySelector(this.config.targetSelector); + renderTarget.append(this.viewportElement); + } + + /** + * get a definition (value) for given name + * @param name name to look up + * @returns value or '' if not found + * @internal + */ + getDefinition(name: string): string { + let ret = ''; + this.definitions.forEach((definitions) => { + const i = definitions.get(name); + if (i !== undefined) { + ret = i; + } + }); + if (ret === '') { + console.log('failed to get definition for', name); + } + return ret; + } + + /** + * create a new module (copy) with given name + * @param name name of module + * @return copy of module or null if not found + * @internal + */ + getNewModule(name: string): RenderTree { + const i = this.modules.get(name); + if (i !== undefined) { + return (new RenderTree(this, i)); + } + console.log('unknown render element', name); + return null; + } + + /** + * handle mouse click or tap events + * @param ev brower event + */ + private handleClick = (ev?: MouseEvent) => { + const target = ev.target; + let action = target.getAttribute('data-action'); + if (action === null) { + action = target.getAttribute("data-id"); + } + let slideNum; + switch (action) { + case 'fullscreen': + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + } else { + document.exitFullscreen(); + } + ev.stopPropagation(); + ev.preventDefault(); + break; + case 'prev': + slideNum = Math.max(this.currentPage - 1, 0); + break; + case 'next': + case null: + slideNum = Math.min(this.currentPage + 1, this.numPages - 1); + break; + } + if (slideNum !== undefined) { + this.changeSlide(slideNum); + } + if (target.closest('.ui')) { + ev.stopPropagation(); + ev.preventDefault(); + } + return false; + } + + /** + * handle a change of the page dropdown + */ + private handleDropdown = (ev?: Event) => { + ev.stopPropagation(); + ev.preventDefault(); + this.changeSlide(parseInt(this.select.value)); + return false; + } + + /** + * handle key presses + * @param ev pressed key + */ + private handleKey = (ev?: KeyboardEvent) => { + switch (ev.key) { + case 'ArrowLeft': + this.changeSlide(Math.max(this.currentPage - 1, 0)); + break; + case 'ArrowRight': + this.changeSlide(Math.min(this.currentPage + 1, this.numPages - 1)); + break; + } + } + + /** + * handle mouse move and show ui + */ + private handleMouseMove = () => { + if (this.timer) { + window.clearTimeout(this.timer); + } + this.ui.classList.add('visible'); + document.body.style.cursor = 'auto'; + this.timer = window.setTimeout(this.hideUI, 2000); + } + + /** + * handle end of touch event + * creates slide left or right if swipe was long enough + * @param ev browser touch event + */ + private handleTouchEnd = (ev: TouchEvent) => { + const touchEndX = ev.changedTouches[0].screenX; + if (Math.abs(this.touchStartX - touchEndX) > 100) { + let slideNum; + if (this.touchStartX < touchEndX) { + slideNum = Math.max(this.currentPage - 1, 0); + } else { + slideNum = Math.min(this.currentPage + 1, this.numPages - 1); + } + this.changeSlide(slideNum); + } else { + this.handleMouseMove(); + } + } + + /** + * handle start of touch + * saves position to detect swipes + */ + private handleTouchStart = (ev: TouchEvent) => { + this.touchStartX = ev.changedTouches[0].screenX; + } + + /** + * hide ui + * called by timer function + */ + private hideUI = () => { + this.ui.classList.remove('visible'); + document.body.style.cursor = 'none'; + } + + /** + * move viewpoint to area defined by current page + */ + private moveViewpoint = () => { + const view = (this.tree.getElement(this.currentPage)).getView(); + let transform = ''; + if (view.viewRotation.x) { + transform += ' rotateX(' + (-view.viewRotation.x) + 'deg) ' + transform; + } + if (view.viewRotation.y) { + transform += ' rotateY(' + (-view.viewRotation.y) + 'deg) ' + transform; + } + if (view.viewRotation.z) { + transform += ' rotateZ(' + (-view.viewRotation.z) + 'deg) ' + transform; + } + transform += ' translate3d(' + (-view.position.x) + 'px,' + (-view.position.y) + 'px, ' + (-view.position.z) + 'px)'; + + // calculate zoom + const wc = parseInt(this.getDefinition('width')) / view.size.w; + const hc = parseInt(this.getDefinition('height')) / view.size.h; + const scale = Math.min(wc, hc); + const zoom = this.viewportElement.parentElement.clientHeight / parseFloat(this.getDefinition('height')) * scale; + if (zoom != 1) { + transform = 'scale(' + zoom + ')' + transform; + } + this.viewportElement.style.transform = transform; + } + + /** + * pop defintion list to remove last definition from stack + * @internal + */ + pop() { + this.definitions.pop(); + } + + /** + * add handles for presentation and start it + */ + present() { + window.addEventListener('resize', this.moveViewpoint); + const list = document.querySelectorAll('body, [data-action]'); + list.forEach((e) => { + e.addEventListener('click', this.handleClick); + }); + window.addEventListener('keydown', this.handleKey); + this.select.addEventListener('change', this.handleDropdown); + window.addEventListener('mousemove', this.handleMouseMove); + document.body.addEventListener('touchstart', this.handleTouchStart); + document.body.addEventListener('touchend', this.handleTouchEnd); + this.moveViewpoint(); + this.changeSlide(this.currentPage); + } + + /** + * push a new empty definition list to stack + * @internal + */ + push(): void { + this.definitions.push(new Map()); + } + + /** + * set a value to current definition list on stack + * @internal + */ + set(key: string, value: string): void { + const definition = this.definitions.at(-1); + definition.set(key, value); + } +} diff --git a/src/rendertree/rendertree.ts b/src/rendertree/rendertree.ts new file mode 100644 index 0000000..2fdb0b3 --- /dev/null +++ b/src/rendertree/rendertree.ts @@ -0,0 +1,178 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + **/ + +import { PresentationMaker } from '../presentationmaker'; +import { RTModule } from './rtmodule'; +import { RTNode } from './rtnode'; +import { RTImg } from './rtimg'; +import { RTCalc } from './rtcalc'; +import { RTText } from './rttext'; +import { RTOptions } from './rtoptions'; +import { RTPage } from './rtpage'; + +/** + * the root of render tree + * @internal + */ +export class RenderTree { + /** handle to main instance */ + private main: PresentationMaker; + /** our child elements */ + private elements: RTNode[]; + /** definitions from input */ + private definitions: Map; + + /** + * constructor + * @param pm handle to main instance + * @param other optional other tree to copy data from + */ + constructor(pm: PresentationMaker, other: RenderTree = null) { + this.main = pm; + this.elements = []; + this.definitions = new Map(); + if (other !== null) { + other.elements.forEach((e) => { + this.elements.push(e.clone()); + }); + } + } + + /** + * create elements from given parent + * @param baseElem root element to create new render tree nodes + */ + createFromElem(baseElem: HTMLElement) { + const nodes = baseElem.childNodes; + nodes.forEach((node) => { + if (node.nodeType === 1) { + const newNode = this.createNodeFromElem(node); + if (newNode !== null) { + this.elements.push(newNode); + } + } + }); + } + + + /** + * create a new node from given element + * + * currently supports 'calc', 'img', 'options', 'page', 'text' + * @param elem to create from + */ + createNodeFromElem(elem: HTMLElement): RTNode { + const name = elem.localName; + // final types + switch (name) { + case 'calc': + return RTCalc.fromElem(this, elem); + case 'img': + return RTImg.fromElem(this, elem); + case 'options': + return RTOptions.fromElem(this.main, this, elem); + case 'page': + return RTPage.fromElem(this.main, this, elem); + case 'text': + return RTText.fromElem(this, elem); + } + // still here, probably a module + const tree = this.main.getNewModule(name); + if (tree === null) { + console.log('unknown module', name); + return null; + } + tree.definitions.clear(); + // add attributes to node + for (let i = 0; i < elem.attributes.length; ++i) { + const attr = elem.attributes[i]; + tree.definitions.set(attr.localName, attr.textContent); + } + elem.childNodes.forEach((child) => { + if (child.nodeType === 1) { + const childElem = child; + tree.definitions.set(childElem.localName, childElem.innerHTML); + } + }); + return new RTModule(this, tree); + } + + /** + * get current definition for given name + * @param name name to look up + * @returns value or '' + */ + getDefinition(name: string): string { + return this.main.getDefinition(name); + } + + /** + * get node with given index + * @param num index + * @return node or null if invalid index + */ + getElement(num: number): RTNode { + if (num >= 0 && num <= this.elements.length) { + return this.elements[num]; + } + return null; + } + + /** + * return the number of elements in the root tree + */ + getElementCount(): number { + return this.elements.length; + } + + /** + * get main instance + * @returns main instance + */ + getMain(): PresentationMaker { + return this.main; + } + + /** + * pop defintion list to remove last definition from stack + */ + pop() { + this.main.pop(); + } + + /** + * push a new empty definition list to stack + */ + push(): void { + this.main.push(); + } + + /** + * render tree to given target + * @param target target to render into + */ + render(target: HTMLElement) { + this.main.push(); + // save current definitions on the stack + this.definitions.forEach((v, k) => { + this.main.set(k, v); + }); + this.elements.forEach((element) => { + element.render(target); + }); + this.main.pop(); + } + + /** + * set definition + * @param key key name + * @param value value data + */ + set(key: string, value: string) { + this.main.set(key, value); + } + +}; diff --git a/src/rendertree/rtcalc.ts b/src/rendertree/rtcalc.ts new file mode 100644 index 0000000..951720a --- /dev/null +++ b/src/rendertree/rtcalc.ts @@ -0,0 +1,67 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; +import Formula from 'fparser'; + +/** + * calculate new defintions based on formula + */ +export class RTCalc extends RTNode { + /** name of the definition */ + private target: string; + /** expression to calculate */ + private expression: string; + + /** + * clone function + * @return a copy of us + */ + clone(): RTCalc { + return new RTCalc(this.root, this.target, this.expression); + } + + /** + * constructor + * @param root our parent RenderTree + * @param target name of the new definition + * @param expression expression to calculate + */ + constructor(root: RenderTree, target: string, expression: string) { + super(root); + this.target = target; + this.expression = expression; + } + + /** + * create RTCalc from given node into given RenderTree + * @param root render tree to insert to + * @param node input node + * @returns new RTCalc node + */ + static fromElem(root: RenderTree, node: HTMLElement): RTCalc { + return new RTCalc( + root, + node.getAttribute('name') || '', + node.innerHTML + ); + } + + + /** + * render our element into target + * @param target target to render into + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + render(target: HTMLElement): void { + const exp = this.resolveString(this.expression); + const fObj = new Formula(exp); + const result = fObj.evaluate({}); + this.root.set(this.target, result.toString()); + } +} diff --git a/src/rendertree/rtimg.ts b/src/rendertree/rtimg.ts new file mode 100644 index 0000000..ae7fcb7 --- /dev/null +++ b/src/rendertree/rtimg.ts @@ -0,0 +1,99 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; + +/** + * renders an image from tag + */ +export class RTImg extends RTNode { + /** src URI */ + private src: string; + /** x position */ + private x: string; + /** y position */ + private y: string; + /** width */ + private width: string; + /** height */ + private height: string; + /** mask-image css string */ + private maskImage: string; + + /** + * clone function + * @return a copy of us + */ + clone(): RTImg { + return new RTImg( + this.root, + this.src, + this.x, + this.y, + this.width, + this.height, + this.maskImage + ); + } + + /** + * constructor + * @param root our parent RenderTree + * @param src source URI + * @param x x position + * @param y y position + * @param width image width + * @param height image height + * @param maskImage optional css for mask-image + */ + constructor(root: RenderTree, src: string, x: string, y: string, width: string, height: string, maskImage: string) { + super(root); + this.src = src; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.maskImage = maskImage; + } + + /** + * create RTImg from given node into given RenderTree + * @param root render tree to insert to + * @param node input node + * @returns new RTImg node + */ + static fromElem(root: RenderTree, node: HTMLElement): RTImg { + return new RTImg( + root, + this.getField(node, 'src'), + this.getField(node, 'x'), + this.getField(node, 'y'), + this.getField(node, 'width'), + this.getField(node, 'height'), + this.getField(node, 'mask-image') + ); + } + + /** + * render image to HTML target + * @param target target to render to + */ + render(target: HTMLElement): void { + const img = document.createElement('img'); + img.style.position = 'absolute'; + img.style.top = this.calc(this.resolveString(this.y)) + 'px'; + img.style.left = this.calc(this.resolveString(this.x)) + 'px'; + img.setAttribute('width', this.calc(this.resolveString(this.width)).toString()); + img.setAttribute('height', this.calc(this.resolveString(this.height)).toString()); + img.setAttribute('src', this.resolveString(this.src)); + if (this.maskImage) { + img.style.maskImage = this.maskImage; + } + target.append(img); + } +} diff --git a/src/rendertree/rtmodule.ts b/src/rendertree/rtmodule.ts new file mode 100644 index 0000000..0867213 --- /dev/null +++ b/src/rendertree/rtmodule.ts @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + **/ + +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; + +/** + * custom user declared module + */ +export class RTModule extends RTNode { + /** render tree for our children */ + private tree: RenderTree; + + /** + * clone function + * @return a copy of us + */ + clone(): RTModule { + const tree = new RenderTree(this.root.getMain(), this.tree); + return new RTModule(this.root, tree); + } + + /** + * constructor + * @param root parent RenderTree + * @param tree child renderTree + */ + constructor(root: RenderTree, tree: RenderTree) { + super(root); + this.tree = tree; + } + + /** + * render module content to given target + * @param target target to render to + */ + render(target: HTMLElement): void { + this.tree.render(target); + } +} diff --git a/src/rendertree/rtnode.ts b/src/rendertree/rtnode.ts new file mode 100644 index 0000000..d41dcd1 --- /dev/null +++ b/src/rendertree/rtnode.ts @@ -0,0 +1,105 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { RenderTree } from './rendertree'; +import Formula from 'fparser'; + +/** + * base class for all RenderTree nodes + */ +export abstract class RTNode { + /** root render tree */ + protected root: RenderTree; + + /** + * calculate given expression + * @param exp expression string + * @returns result as number or 0 in case of error + */ + calc(exp: string): number { + if (exp === '') { + return 0; + } + try { + const fObj = new Formula(exp); + return fObj.evaluate({}); + } catch(e) { + } + return 0; + } + + /** + * abstract function to clone the node + * must be implemented by every sub class + */ + abstract clone(): RTNode; + + /** + * constructor + * @param root parent RenderTree + */ + constructor(root: RenderTree) { + this.root = root; + } + + /** + * helper function to get value from attribute or from named child node + * @param node current HTML element + * @param name name of element to fetch + * @return value or '' in case of error + */ + static getField(node: HTMLElement, name: string): string { + // try attribute + let field = node.getAttribute(name); + // try child nodes + node.childNodes.forEach((child) => { + if (child.nodeType === 1) { + const childElem = child; + if (childElem.localName === name) { + field = childElem.innerHTML; + } + } + }); + return field; + } + + /** + * abstract function to render the node + * must be implemented by every sub class + * @param target target to render into + */ + abstract render(target: HTMLElement): void; + + /** + * resolve a string using variable replacements + * @param input input string + * @returns input strings with replaced variables + */ + resolveString(input: string): string { + if (!input || input.indexOf('$') < 0) { + return input; + } + let str = input; + let dollar; + let lastDollar = 0; + while ((dollar = str.indexOf('${', lastDollar)) >= 0) { + if (str.at(dollar - 1) == '\\') { + str = str.substring(0, dollar - 1) + str.substring(dollar); + lastDollar = dollar + 1; + continue; + } + const end = str.indexOf('}', dollar); + if (end < 0) break; + const search = str.substring(dollar + 2, end); + const replace = this.root.getDefinition(search); + str = str.replace(str.substring(dollar, end + 1), replace); + } + return (str); + } + + +} diff --git a/src/rendertree/rtoptions.ts b/src/rendertree/rtoptions.ts new file mode 100644 index 0000000..dbbd62c --- /dev/null +++ b/src/rendertree/rtoptions.ts @@ -0,0 +1,125 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { PresentationMaker } from '../presentationmaker'; +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; + +/** + * transform subtree by given options + */ +export class RTOptions extends RTNode { + /** blend mode */ + private blendmode: string; + /** flip axis */ + private flip: string; + /** opactity */ + private opacity: string; + /** rotate */ + private rotate: string; + /** subtee to transform */ + private subtree: RenderTree; + /** y position when flipping */ + private y: string; + + /** + * clone function + * @return a copy of us + */ + clone(): RTOptions { + return new RTOptions( + this.root, + this.subtree, + this.opacity, + this.blendmode, + this.flip, + this.rotate, + this.y + ); + } + + /** + * constructor + * @param root parent RenderTree + * @param subtree child sub tree + * @param opacity opacity value + * @param blendmode blend mode + * @param flip flip axis + * @param rotate rotation in degree X, Y, Z + * @param y y position when flipping + */ + constructor(root: RenderTree, subtree: RenderTree, opacity: string, blendmode: string, flip: string, rotate: string, y: string) { + super(root); + this.subtree = subtree; + this.opacity = opacity; + this.blendmode = blendmode; + this.flip = flip; + this.rotate = rotate; + this.y = y; + } + + /** + * create RTOptions from given node into given RenderTree + * @param root render tree to insert to + * @param node input node + * @returns new RTOptions node + */ + static fromElem(main: PresentationMaker, root: RenderTree, node: HTMLElement): RTOptions { + const subtree = new RenderTree(main); + subtree.createFromElem(node); + return new RTOptions( + root, + subtree, + this.getField(node, 'opacity') || '', + this.getField(node, 'blend') || '', + this.getField(node, 'flip') || '', + this.getField(node, 'rotate') || '', + this.getField(node, 'y') || '' + ); + } + + /** + * render our element into target + * @param target target to render into + */ + render(target: HTMLElement): void { + const div = document.createElement('div'); + div.classList.add('option'); + if (this.opacity !== '') { + div.style.opacity = this.opacity; + } + if (this.blendmode !== '') { + div.style.mixBlendMode = this.blendmode; + } + if (this.flip !== '') { + if (this.flip == 'v') { + div.style.transform = 'scaleY(-1)'; + } + } + if (this.y) { + div.style.position = 'absolute'; + div.style.bottom = '0px'; + } + if (this.rotate !== '') { + const r = this.rotate.split(','); + let transform = ''; + if (r[0]) { + transform += 'rotateX(' + (this.calc(this.resolveString(r[0]))) + 'deg)'; + } + if (r[1]) { + transform += 'rotateY(' + (this.calc(this.resolveString(r[1]))) + 'deg)'; + } + if (r[2]) { + transform += 'rotateZ(' + (this.calc(this.resolveString(r[2]))) + 'deg)'; + } + div.style.transform = transform; + div.style.transformOrigin = 'top left'; + } + this.subtree.render(div); + target.append(div); + } +} diff --git a/src/rendertree/rtpage.ts b/src/rendertree/rtpage.ts new file mode 100644 index 0000000..985c1db --- /dev/null +++ b/src/rendertree/rtpage.ts @@ -0,0 +1,257 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; +import { PresentationMaker } from '../presentationmaker'; + +/** + * view transformations as strings + */ +export interface TransformationS { + /** position on HTML space */ + position: {x: string, y: string, z: string}; + /** rotation in HTML space */ + rotation: {x: string, y: string, z: string}; + /** page size in pixels */ + size: {w: string, h: string}; + /** view position in HTML space */ + viewpos: {x: string, y: string, z: string}; + /** view size in pixels */ + viewSize: {w: string, h: string}; + /** view rotation in degree */ + viewRotation: {x: string, y: string, z: string}; +} + +/** + * view transformation as numbers + */ +export interface RTTransformation { + /** view position */ + position: {x: number, y: number, z: number}; + /** view size */ + size: {w: number, h: number}; + /** view rotation in degrees */ + viewRotation: {x: number, y: number, z: number}; +} + +/** + * page node + */ +export class RTPage extends RTNode { + /** page id */ + private id: string; + /** page name */ + private name: string; + /** tree to render on page */ + private subtree: RenderTree; + /** tranform informations */ + private transformation: TransformationS; + + /** + * clone function + * @return a copy of us + */ + clone(): RTPage { + return new RTPage( + this.root, + this.subtree, + this.id, + this.transformation.size.w, + this.transformation.size.h, + this.transformation.position.x, + this.transformation.position.y, + this.transformation.position.z, + this.transformation.rotation.y, + this.transformation.rotation.y, + this.transformation.rotation.z, + this.transformation.viewpos.x, + this.transformation.viewpos.y, + this.transformation.viewpos.z, + this.transformation.viewSize.w, + this.transformation.viewSize.h, + this.transformation.viewRotation.x, + this.transformation.viewRotation.y, + this.transformation.viewRotation.z, + this.name + ) + } + + /** + * constructor + * @param root parent RenderTree + * @param subtree RenderTree for page content + * @param id page id + * @param width page width + * @param height page height + * @param x x position + * @param y y position + * @param z z position + * @param rx rotation on X-axis + * @param ry rotation on Y-axis + * @param rz rotation on Z-axis + * @param vx view position on X-axis + * @param vy view position on Y-axis + * @param vz view position on Z-axis + * @param vrx view rotation on X-axis + * @param vry view rotation on Y-axis + * @param vrz view rotation on Z-axis + */ + constructor(root: RenderTree, subtree: RenderTree, id: string, width: string, height: string, x: string, y: string, z: string, rx: string, ry: string, rz:string, vx: string, vy: string, vz: string, vw: string, vh: string, vrx: string, vry: string, vrz: string, name: string) { + super(root); + this.subtree = subtree; + this.id = id; + this.transformation = { + size: { w: width, h: height}, + position: {x: x, y: y, z: z}, + rotation: {x: rx, y: ry, z: rz}, + viewpos: {x: vx, y: vy, z: vz}, + viewSize: {w: vw, h: vh}, + viewRotation: {x: vrx, y: vry, z: vrz} + }; + this.name = name; + } + + /** + * create RTPage from given node into given RenderTree + * @param root render tree to insert to + * @param node input node + * @returns new RTOptions node + */ + static fromElem(main:PresentationMaker, root: RenderTree, node: HTMLElement): RTPage { + const subtree = new RenderTree(main); + subtree.createFromElem(node); + return new RTPage( + root, + subtree, + node.getAttribute('id') || '', + node.getAttribute('width') || '${width}', + node.getAttribute('height') || '${height}', + node.getAttribute('x') || '0', + node.getAttribute('y') || '0', + node.getAttribute('z') || '0', + node.getAttribute('rx') || '0', + node.getAttribute('ry') || '0', + node.getAttribute('rz') || '0', + node.getAttribute('vx') || '', + node.getAttribute('vy') || '', + node.getAttribute('vz') || '', + node.getAttribute('vwidth') || '', + node.getAttribute('vheight') || '', + node.getAttribute('vrx') || '', + node.getAttribute('vry') || '', + node.getAttribute('vrz') || '', + node.getAttribute('name') || '${page}', + ); + } + + /** + * get page name + * @returns page name + */ + getName(): string { + return this.name; + } + + /** + * get view transformation + * @return RTTransformation + */ + getView(): RTTransformation { + let position; + if (this.transformation.viewpos.x !== '' || this.transformation.viewpos.y !== '' || this.transformation.viewpos.z !=='') { + position = { + x: this.calc(this.resolveString(this.transformation.viewpos.x)), + y: this.calc(this.resolveString(this.transformation.viewpos.y)), + z: this.calc(this.resolveString(this.transformation.viewpos.z)) + }; + } else { + position = { + x: this.calc(this.resolveString(this.transformation.position.x)), + y: this.calc(this.resolveString(this.transformation.position.y)), + z: this.calc(this.resolveString(this.transformation.position.z)) + } + } + let size; + if (this.transformation.viewSize.w !== '' || this.transformation.viewSize.h !== '') { + size = { + w: this.calc(this.resolveString(this.transformation.viewSize.w)), + h: this.calc(this.resolveString(this.transformation.viewSize.h)) + }; + } else { + size = { + w: this.calc(this.resolveString(this.transformation.size.w)), + h: this.calc(this.resolveString(this.transformation.size.h)) + } + } + let viewRotation; + if (this.transformation.viewRotation.x !== '' || this.transformation.viewRotation.y !== '' || this.transformation.viewRotation.z !== '') { + viewRotation = { + x: this.calc(this.resolveString(this.transformation.viewRotation.x)), + y: this.calc(this.resolveString(this.transformation.viewRotation.y)), + z: this.calc(this.resolveString(this.transformation.viewRotation.z)) + } + } else { + viewRotation = { + x: 0, + y: 0, + z: 0 + } + } + return { + position: position, + size: size, + viewRotation: viewRotation + }; + } + + /** + * render our element into target + * @param target target to render into + */ + render(target: HTMLElement): void { + this.root.set('page', (parseInt(this.root.getDefinition('page')) + 1).toString()); + const pageDiv = document.createElement('article'); + pageDiv.classList.add('page'); + pageDiv.classList.add('inactive'); + let transform = ''; + const x = this.calc(this.resolveString(this.transformation.position.x)); + const y = this.calc(this.resolveString(this.transformation.position.y)); + const z = this.calc(this.resolveString(this.transformation.position.z)); + if (x || y || z) { + transform += 'translate3d(' + x + 'px, ' + y + 'px, ' + z + 'px)'; + } + const rx = -this.calc(this.resolveString(this.transformation.rotation.x)); + const ry = -this.calc(this.resolveString(this.transformation.rotation.y)); + const rz = -this.calc(this.resolveString(this.transformation.rotation.z)); + if (rx) { + transform += 'rotateX(' + rx + 'deg)'; + } + if (ry) { + transform += 'rotateY(' + ry + 'deg)'; + } + if (rz) { + transform += 'rotateZ(' + rz + 'deg)'; + } + if (transform !== '') { + pageDiv.style.transform = transform; + } + const width = this.calc(this.resolveString(this.transformation.size.w)).toString(); + const height = this.calc(this.resolveString(this.transformation.size.h)).toString(); + pageDiv.style.width = width + 'px'; + pageDiv.style.height = height + 'px'; + if (this.id) { + pageDiv.id = this.id; + } + target.append(pageDiv); + this.root.push() + this.subtree.set('width', width); + this.subtree.set('height', height); + this.subtree.render(pageDiv); + this.root.pop() + } +} diff --git a/src/rendertree/rttext.ts b/src/rendertree/rttext.ts new file mode 100644 index 0000000..33f3947 --- /dev/null +++ b/src/rendertree/rttext.ts @@ -0,0 +1,179 @@ +/** + * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop + * SPDX-License-Identifier: GPL-3.0-or-later + * @author Author: Sascha Nitsch (grumpydeveloper) + * @internal + **/ + +import { RTNode } from './rtnode'; +import { RenderTree } from './rendertree'; + +/** + * Text node + */ +export class RTText extends RTNode { + /** left position of text box */ + private x0: string; + /** top position of text box */ + private y0: string; + /** rigth position of text box */ + private x1: string; + /** left bottom of text box */ + private y1: string; + /** width of text box */ + private width: string; + /** height of text box */ + private height: string; + /** compact list of [left, top, right, bottom] */ + private pos: string; + /** horizontal alignment of text in box */ + private halign: string; + /** vertical alignment of text in box */ + private valign: string; + /** font size */ + private fontSize: string; + /** actual text */ + private text: string; + /** text color */ + private color: string; + + /** + * clone function + * @return a copy of us + */ + clone(): RTText { + return new RTText( + this.root, + this.x0, + this.y0, + this.x1, + this.y1, + this.width, + this.height, + this.pos, + this.halign, + this.valign, + this.fontSize, + this.text, + this.color + ); + } + + /** + * create Node from HTMLElement + * @internal + * */ + static fromElem(root: RenderTree, node: HTMLElement): RTText { + return new RTText( + root, + node.getAttribute('x0'), + node.getAttribute('y0'), + node.getAttribute('x1'), + node.getAttribute('y1'), + node.getAttribute('w'), + node.getAttribute('h'), + node.getAttribute('pos'), + node.getAttribute('halign') || 'center', + node.getAttribute('valign'), + node.getAttribute('size'), + node.innerHTML, + node.getAttribute('color') || root.getDefinition('defaultfontcolor') + ); + } + + /** + * constructor + * @param root parent RenderTree + * @param x0 left position of text box + * @param y0 top position of text box + * @param width width of text box + * @param height heightof text box + * @param pos short version of [left, top, right, bottom] + * @param halign horizontal alignment in text box + * @param valign vertical alignment in text box + * @param fontSize font size + * @param text actural text + * @param color text color + */ + constructor(root: RenderTree, x0: string, y0: string, x1: string, y1: string, width: string, height: string, pos: string, halign: string, valign: string, fontSize: string, text: string, color: string) { + super(root); + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + this.width = width; + this.height = height; + this.pos = pos; + this.halign = halign; + this.valign = valign; + this.fontSize = fontSize; + this.text = text; + this.color = color; + } + + /** + * render image to HTML target + * @param target target to render to + */ + render(target: HTMLElement): void { + let x0: number; + let y0: number; + let x1: number; + let y1: number; + if (this.pos) { + const posLookup = this.resolveString(this.pos); + const tokens = posLookup.split(','); + if (tokens.length != 4) { + return; + } + x0 = this.calc(this.resolveString(tokens[0])); + y0 = this.calc(this.resolveString(tokens[1])); + x1 = this.calc(this.resolveString(tokens[2])); + y1 = this.calc(this.resolveString(tokens[3])); + } else { + x0 = this.calc(this.resolveString(this.x0)); + y0 = this.calc(this.resolveString(this.y0)); + if (this.x1) { + x1 = this.calc(this.resolveString(this.x1)); + y1 = this.calc(this.resolveString(this.y1)); + } else { + x1 = x0 + this.calc(this.resolveString(this.width)); + y1 = y0 + this.calc(this.resolveString(this.height)); + } + } + const halign = this.resolveString(this.halign); + const valign = this.resolveString(this.valign); + const text = this.resolveString(this.text); + const div = document.createElement('div'); + if (this.color) { + div.style.color = this.resolveString(this.color); + } + div.style.position = 'absolute'; + if (this.fontSize) { + div.style.fontSize = this.calc(this.resolveString(this.fontSize)) + 'px'; + } + div.style.left = x0 + 'px'; + div.style.top = y0 + 'px'; + div.style.width = (x1 - x0) + 'px'; + div.style.height = (y1 - y0) + 'px'; + div.style.overflow = 'clip'; + switch (halign) { + case 'center': + div.style.textAlign = 'center'; + break; + case 'right': + div.style.textAlign = 'right'; + break; + } + switch (valign) { + case 'center': + div.style.alignContent = 'center'; + break; + case 'bottom': + div.style.alignContent = 'end'; + break; + } + div.insertAdjacentHTML('afterbegin', text); + target.append(div); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8e327c1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "ES6", + "target": "ES6", + "allowJs": true, + "moduleResolution": "Bundler", + }, + "typedocOptions": { + "entryPoints": ["./src", "./src/rendertree"], + "entryPointStrategy": "expand", + "out": "docs", + "excludePrivate": false, + }, + "visibilityFilters": { + "protected": true, + "private": true, + "inherited": true, + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..46e593a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,23 @@ +const path = require('path'); + +module.exports = { + entry: './src/presentationmaker.ts', + mode: 'production', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'presentationmaker.js', + libraryTarget: 'global', + }, +};