commit c3d91d124562f69ca775598b70ba8e22a2d982e4 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..8c93a42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +docs +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd63bf2 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# 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. + +## 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 + 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..4436b59 --- /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', + }, +};