diff --git a/packages/abstract-document/src/abstract-document-exporters/pdf/render-image.ts b/packages/abstract-document/src/abstract-document-exporters/pdf/render-image.ts index 2ba6655a..623fbaeb 100644 --- a/packages/abstract-document/src/abstract-document-exporters/pdf/render-image.ts +++ b/packages/abstract-document/src/abstract-document-exporters/pdf/render-image.ts @@ -100,6 +100,12 @@ function abstractComponentToPdf( } }); + // svgToPdfKit doesn't seem to have great support currently, need to find workaround. + + // if (component.rotation) { + // svgUpdated = `${svgUpdated}`; + // } + const imageWidth = component.bottomRight.x - component.topLeft.x; const imageHeight = component.bottomRight.y - component.topLeft.y; svgToPdfKit(pdf, svgUpdated, component.topLeft.x, component.topLeft.y, { diff --git a/packages/abstract-image/src/exporters/react-svg-export-image.tsx b/packages/abstract-image/src/exporters/react-svg-export-image.tsx index 5c611077..7e355421 100644 --- a/packages/abstract-image/src/exporters/react-svg-export-image.tsx +++ b/packages/abstract-image/src/exporters/react-svg-export-image.tsx @@ -3,6 +3,7 @@ import * as B64 from "base64-js"; import * as React from "react"; import * as AbstractImage from "../model/index"; import { TextEncoder } from "util"; +import { Point3D } from "../model/index"; export interface ReactSvgCallbacks { readonly onClick?: MouseCallback; @@ -98,6 +99,7 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; @@ -113,48 +115,48 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; case "text": if (!component.text) { return []; } - const lineHeight = component.fontSize; - const shadowStyle = { + const baseStyle = { textAnchor: getTextAnchor(component.horizontalGrowthDirection), fontSize: component.fontSize.toString() + "px", fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight, fontFamily: component.fontFamily, + }; + + const shadowStyle = { + ...baseStyle, stroke: colorToRgb(component.strokeColor), strokeWidth: component.strokeThickness, }; - const style = { - textAnchor: getTextAnchor(component.horizontalGrowthDirection), - fontSize: component.fontSize.toString() + "px", - fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight, - fontFamily: component.fontFamily, - fill: colorToRgb(component.textColor), - }; - const dy = getBaselineAdjustment(component.verticalGrowthDirection); + const style = { ...baseStyle, fill: colorToRgb(component.textColor) }; - const transform = - "rotate(" + - component.clockwiseRotationDegrees.toString() + - " " + - component.position.x.toString() + - " " + - component.position.y.toString() + - ")"; + // component.clockwiseRotationDegrees is legacy + const transform = component.rotation + ? rotationTransform(component.rotation) + : "rotate(" + + component.clockwiseRotationDegrees.toString() + + " " + + component.position.x.toString() + + " " + + component.position.y.toString() + + ")"; + const dy = getBaselineAdjustment(component.verticalGrowthDirection); const lines: Array = component.text !== null ? component.text.split("\n") : []; const tSpans = lines.map((t) => renderLine( t, component.position.x, - component.position.y + (lines.indexOf(t) + dy) * lineHeight, + component.position.y + (lines.indexOf(t) + dy) * component.fontSize, component.fontSize, - lineHeight + component.fontSize ) ); let cs: Array> = []; @@ -189,6 +191,7 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; case "polyline": @@ -202,6 +205,7 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; case "polygon": @@ -216,6 +220,7 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; case "rectangle": @@ -232,6 +237,7 @@ function _visit(key: string, component: AbstractImage.Component): Array, ]; default: @@ -280,6 +286,13 @@ function renderLine(text: string, x: number, y: number, fontSize: number, lineHe ); } +const rotationStyle = (rotation: Point3D): { readonly style: React.CSSProperties } => ({ + style: { transform: rotationTransform(rotation) }, +}); + +const rotationTransform = (rotation: Point3D): string => + `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`; + function getBaselineAdjustment(d: AbstractImage.GrowthDirection): number { if (d === "up") { return 0.0; diff --git a/packages/abstract-image/src/exporters/svg-export-image.ts b/packages/abstract-image/src/exporters/svg-export-image.ts index c9dc869e..64ca1d10 100644 --- a/packages/abstract-image/src/exporters/svg-export-image.ts +++ b/packages/abstract-image/src/exporters/svg-export-image.ts @@ -1,5 +1,6 @@ import * as B64 from "base64-js"; import * as AbstractImage from "../model/index"; +import { Point3D } from "../model/index"; export function createSVG(image: AbstractImage.AbstractImage, pixelWidth?: number, pixelHeight?: number): string { const imageElements = image.components.map((c: AbstractImage.Component) => abstractComponentToSVG(c)); @@ -36,6 +37,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { width: (component.bottomRight.x - component.topLeft.x).toString(), height: (component.bottomRight.y - component.topLeft.y).toString(), href: url, + style: rotationStyle(component.rotation), }, [] ); @@ -52,6 +54,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { stroke: colorToRgb(component.strokeColor), strokeOpacity: colorToOpacity(component.strokeColor), strokeWidth: component.strokeThickness.toString(), + style: rotationStyle(component.rotation), }, [] ); @@ -64,6 +67,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { stroke: colorToRgb(component.strokeColor), strokeOpacity: colorToOpacity(component.strokeColor), strokeWidth: component.strokeThickness.toString(), + style: rotationStyle(component.rotation), }, [] ); @@ -71,37 +75,37 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { if (!component.text) { return ""; } - const lineHeight = component.fontSize; - const shadowStyle = { + const baseStyle = { textAnchor: getTextAnchor(component.horizontalGrowthDirection), fontSize: component.fontSize.toString() + "px", fontWeight: component.fontWeight, fontFamily: component.fontFamily, + }; + + const shadowStyle = { + ...baseStyle, stroke: colorToRgb(component.strokeColor), strokeOpacity: colorToOpacity(component.strokeColor), strokeWidth: component.strokeThickness.toString() + "px", }; - const style = { - textAnchor: getTextAnchor(component.horizontalGrowthDirection), - fontSize: component.fontSize.toString() + "px", - fontWeight: component.fontWeight, - fontFamily: component.fontFamily, + ...baseStyle, fill: colorToRgb(component.textColor), fillOpacity: colorToOpacity(component.textColor), }; const dy = getBaselineAdjustment(component.verticalGrowthDirection); - - const transform = - "rotate(" + - component.clockwiseRotationDegrees.toString() + - " " + - component.position.x.toString() + - " " + - component.position.y.toString() + - ")"; + // component.clockwiseRotationDegrees is legacy + const transform = component.rotation + ? rotationTransform(component.rotation) + : "rotate(" + + component.clockwiseRotationDegrees.toString() + + " " + + component.position.x.toString() + + " " + + component.position.y.toString() + + ")"; const lines: Array = component.text !== null ? component.text.split("\n") : []; @@ -110,8 +114,8 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { "tspan", { x: component.position.x.toString(), - y: (component.position.y + (lines.indexOf(t) + dy) * lineHeight).toString(), - height: lineHeight.toString() + "px", + y: (component.position.y + (lines.indexOf(t) + dy) * component.fontSize).toString(), + height: component.fontSize.toString() + "px", }, [ t @@ -163,6 +167,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { strokeWidth: component.strokeThickness.toString(), fill: colorToRgb(component.fillColor), fillOpacity: colorToOpacity(component.fillColor), + style: rotationStyle(component.rotation), }, [] ); @@ -176,6 +181,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { strokeWidth: component.strokeThickness.toString(), fill: colorToRgb(component.fillColor), fillOpacity: colorToOpacity(component.fillColor), + style: rotationStyle(component.rotation), }, [] ); @@ -192,6 +198,7 @@ function abstractComponentToSVG(component: AbstractImage.Component): string { strokeWidth: component.strokeThickness.toString(), fill: colorToRgb(component.fillColor), fillOpacity: colorToOpacity(component.fillColor), + style: rotationStyle(component.rotation), }, [] ); @@ -291,6 +298,12 @@ function colorToOpacity(color: AbstractImage.Color): string { return (color.a / 255).toString(); } +const rotationStyle = (rotation: Point3D | undefined): string => + rotation ? `transform: ${rotationTransform(rotation)}` : ""; + +const rotationTransform = (rotation: Point3D): string => + `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`; + function getImageUrl(format: AbstractImage.BinaryFormat, data: AbstractImage.ImageData): string { if (data.type === "url") { return data.url; diff --git a/packages/abstract-image/src/model/component.ts b/packages/abstract-image/src/model/component.ts index 7b3982c7..d3d93183 100644 --- a/packages/abstract-image/src/model/component.ts +++ b/packages/abstract-image/src/model/component.ts @@ -31,6 +31,7 @@ export interface BinaryImage { readonly format: BinaryFormat; readonly data: ImageData; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export type ImageData = ImageBytes | ImageUrl; @@ -50,7 +51,8 @@ export function createBinaryImage( bottomRight: Point.Point, format: BinaryFormat, data: ImageData, - id?: string + id?: string, + rotation?: Point.Point & { readonly z: number } ): BinaryImage { return { type: "binaryimage", @@ -59,6 +61,7 @@ export function createBinaryImage( format: format, data: data, id: id, + rotation, }; } @@ -70,6 +73,7 @@ export interface Ellipse { readonly strokeThickness: number; readonly fillColor: Color.Color; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export function createEllipse( @@ -78,7 +82,8 @@ export function createEllipse( strokeColor: Color.Color, strokeThickness: number, fillColor: Color.Color, - id?: string + id?: string, + rotation?: Point.Point3D ): Ellipse { return { type: "ellipse", @@ -88,6 +93,7 @@ export function createEllipse( strokeThickness: strokeThickness, fillColor: fillColor, id: id, + rotation, }; } @@ -98,6 +104,7 @@ export interface Line { readonly strokeColor: Color.Color; readonly strokeThickness: number; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export function createLine( @@ -105,7 +112,8 @@ export function createLine( end: Point.Point, strokeColor: Color.Color, strokeThickness: number, - id?: string + id?: string, + rotation?: Point.Point3D ): Line { return { type: "line", @@ -114,6 +122,7 @@ export function createLine( strokeColor: strokeColor, strokeThickness: strokeThickness, id: id, + rotation, }; } @@ -123,13 +132,15 @@ export interface PolyLine { readonly strokeColor: Color.Color; readonly strokeThickness: number; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export function createPolyLine( points: Array, strokeColor: Color.Color, strokeThickness: number, - id?: string + id?: string, + rotation?: Point.Point3D ): PolyLine { return { type: "polyline", @@ -137,6 +148,7 @@ export function createPolyLine( strokeColor: strokeColor, strokeThickness: strokeThickness, id: id, + rotation, }; } @@ -147,6 +159,7 @@ export interface Polygon { readonly strokeThickness: number; readonly fillColor: Color.Color; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export function createPolygon( @@ -154,7 +167,8 @@ export function createPolygon( strokeColor: Color.Color, strokeThickness: number, fillColor: Color.Color, - id?: string + id?: string, + rotation?: Point.Point3D ): Polygon { return { type: "polygon", @@ -163,6 +177,7 @@ export function createPolygon( strokeThickness: strokeThickness, fillColor: fillColor, id: id, + rotation, }; } @@ -174,6 +189,7 @@ export interface Rectangle { readonly strokeThickness: number; readonly fillColor: Color.Color; readonly id: string | undefined; + readonly rotation?: Point.Point3D | undefined; } export function createRectangle( @@ -182,7 +198,8 @@ export function createRectangle( strokeColor: Color.Color, strokeThickness: number, fillColor: Color.Color, - id?: string + id?: string, + rotation?: Point.Point3D ): Rectangle { return { type: "rectangle", @@ -192,6 +209,7 @@ export function createRectangle( strokeThickness: strokeThickness, fillColor: fillColor, id: id, + rotation, }; } @@ -225,6 +243,7 @@ export interface Text { readonly strokeThickness: number; readonly strokeColor: Color.Color; readonly italic: boolean; + readonly rotation?: Point.Point3D | undefined; } export function createText( @@ -240,7 +259,8 @@ export function createText( verticalGrowthDirection: GrowthDirection, strokeThickness: number, strokeColor: Color.Color, - italic: boolean + italic: boolean, + rotation?: Point.Point3D ): Text { return { type: "text", @@ -257,6 +277,7 @@ export function createText( strokeThickness: strokeThickness, strokeColor: strokeColor, italic: italic, + rotation, }; } diff --git a/packages/abstract-image/src/model/point.ts b/packages/abstract-image/src/model/point.ts index 0ff4f9be..ef137f15 100644 --- a/packages/abstract-image/src/model/point.ts +++ b/packages/abstract-image/src/model/point.ts @@ -6,6 +6,14 @@ export interface Point { export function createPoint(x: number, y: number): Point { return { x: x, - y: y + y: y, }; } + +export interface Point3D { + readonly x: number; + readonly y: number; + readonly z: number; +} + +export const createPoint3D = (x: number, y: number, z: number): Point3D => ({ x, y, z });