Skip to content

Commit 395ca65

Browse files
committed
February 2023 Release of the APL 2023.1 compliant APL Viewhost Web
For more details on this release refer to CHANGELOG.md To learn about APL see: https://developer.amazon.com/docs/alexa-presentation-language/understand-apl.html
1 parent 160ee98 commit 395ca65

34 files changed

+989
-210
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog for apl-viewhost-web
22

3+
## [2023.1]
4+
This release adds support for version 2023.1 of the APL specification. Please also see APL Core Library for changes: [apl-core-library CHANGELOG](https://github.com/alexa/apl-core-library/blob/master/CHANGELOG.md)
5+
6+
### Added
7+
- SRT support for APL Video textTrack
8+
- Support for new Porter-Duff blend modes
9+
10+
### Changed
11+
- Bug fixes
12+
313
## [2022.2]
414
This release adds support for version 2022.2 of the APL specification. Please also see APL Core Library for changes: [apl-core-library CHANGELOG](https://github.com/alexa/apl-core-library/blob/master/CHANGELOG.md)
515

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Alexa Presentation Language (APL) Viewhost Web
22

33
<p>
4-
<a href="https://github.com/alexa/apl-viewhost-web/tree/v2022.2.0" alt="version">
5-
<img src="https://img.shields.io/badge/stable%20version-2022.2.0-brightgreen" /></a>
6-
<a href="https://github.com/alexa/apl-core-library/tree/v2022.2.0" alt="APLCore">
7-
<img src="https://img.shields.io/badge/apl%20core%20library-2022.2.0-navy" /></a>
4+
<a href="https://github.com/alexa/apl-viewhost-web/tree/v2023.1.0" alt="version">
5+
<img src="https://img.shields.io/badge/stable%20version-2023.1.0-brightgreen" /></a>
6+
<a href="https://github.com/alexa/apl-core-library/tree/v2023.1.0" alt="APLCore">
7+
<img src="https://img.shields.io/badge/apl%20core%20library-2023.1.0-navy" /></a>
88
</p>
99

1010
## Introduction
@@ -16,7 +16,7 @@ platform or framework for which the view host was designed by leveraging the fun
1616

1717
### Prerequisites
1818

19-
* [NodeJS](https://nodejs.org/en/) - version 10.x or higher
19+
* [NodeJS](https://nodejs.org/en/) - version 16.x or higher
2020
* [cmake](https://cmake.org/install/) - the easiest way to install on Mac is using `brew install cmake`
2121
* [Yarn](https://yarnpkg.com/getting-started/install)
2222

js/apl-html/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "apl-html",
2+
"name": "@amzn/apl-html",
33
"version": "1.0.0",
44
"license": "SEE LICENSE IN LICENSE.txt",
55
"main": "lib/index.js",
@@ -52,5 +52,8 @@
5252
"webpack-cli": "^3.3.12",
5353
"webpack-merge": "^4.2.1",
5454
"xregexp": "4.2.4"
55+
},
56+
"npm-pretty-much": {
57+
"legacyPackageNameAlias": "apl-html"
5558
}
5659
}

js/apl-html/src/CommandFactory.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import APLRenderer from './APLRenderer';
7+
import { EventType } from './enums/EventType';
8+
import { DataSourceFetchRequest } from './events/DataSourceFetchRequest';
9+
import { ExtensionEvent } from './events/ExtensionEvent';
10+
import { Finish } from './events/Finish';
11+
import { Focus } from './events/Focus';
12+
import { LineHighlight } from './events/LineHighlight';
13+
import { MediaRequest } from './events/MediaRequest';
14+
import { OpenUrl } from './events/OpenUrl';
15+
import { ReInflate} from './events/ReInflate';
16+
import { RequestLineBounds } from './events/RequestLineBounds';
17+
import { SendEvent } from './events/SendEvent';
18+
19+
/**
20+
* Creates and executes a command
21+
* @param event The core engine event
22+
* @param renderer A reference to the renderer instance
23+
* @internal
24+
*/
25+
export const commandFactory = (event: APL.Event, renderer: APLRenderer) => {
26+
if (factoryMap[event.getType()]) {
27+
return factoryMap[event.getType()](event, renderer);
28+
}
29+
throw new Error(`Cannot create command with type ${event.getType()}`);
30+
};
31+
32+
const factoryMap = {
33+
[EventType.kEventTypeSendEvent]: (event: APL.Event, renderer: APLRenderer) => {
34+
const command = new SendEvent(event, renderer);
35+
command.execute();
36+
return command;
37+
},
38+
[EventType.kEventTypeRequestLineBounds]: (event: APL.Event, renderer: APLRenderer) => {
39+
const command = new RequestLineBounds(event, renderer);
40+
command.execute();
41+
return command;
42+
},
43+
[EventType.kEventTypeLineHighlight]: (event: APL.Event, renderer: APLRenderer) => {
44+
const command = new LineHighlight(event, renderer);
45+
command.execute();
46+
return command;
47+
},
48+
[EventType.kEventTypeReinflate]: (event: APL.Event, renderer: APLRenderer) => {
49+
const command = new ReInflate(event, renderer);
50+
command.execute();
51+
return command;
52+
},
53+
[EventType.kEventTypeFinish]: (event: APL.Event, renderer: APLRenderer) => {
54+
const command = new Finish(event, renderer);
55+
command.execute();
56+
return command;
57+
},
58+
[EventType.kEventTypeFocus]: (event: APL.Event, renderer: APLRenderer) => {
59+
const command = new Focus(event, renderer);
60+
command.execute();
61+
return command;
62+
},
63+
[EventType.kEventTypeOpenURL]: (event: APL.Event, renderer: APLRenderer) => {
64+
const command = new OpenUrl(event, renderer);
65+
command.execute();
66+
return command;
67+
},
68+
[EventType.kEventTypeDataSourceFetchRequest]: (event: APL.Event, renderer: APLRenderer) => {
69+
const command = new DataSourceFetchRequest(event, renderer);
70+
command.execute();
71+
return command;
72+
},
73+
[EventType.kEventTypeExtension]: (event: APL.Event, renderer: APLRenderer) => {
74+
const command = new ExtensionEvent(event, renderer);
75+
command.execute();
76+
return command;
77+
},
78+
[EventType.kEventTypeMediaRequest]: (event: APL.Event, renderer: APLRenderer) => {
79+
const command = new MediaRequest(event, renderer);
80+
command.execute();
81+
return command;
82+
}
83+
};

js/apl-html/src/ComponentFactory.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export const componentFactory = (renderer: APLRenderer, component: APL.Component
2424
parent?: Component, ensureLayout: boolean = false,
2525
insertAt: number = -1 ): Component<IGenericPropType> => {
2626
const id = component.getUniqueId();
27+
let comp;
2728
if (renderer.componentMap[id]) {
28-
const comp = renderer.componentMap[id];
29+
comp = renderer.componentMap[id];
2930
comp.parent = parent;
3031
if (ensureLayout && comp instanceof Text) {
3132
comp.setDimensions();
@@ -40,25 +41,29 @@ export const componentFactory = (renderer: APLRenderer, component: APL.Component
4041
}
4142
return comp;
4243
} else if (factoryMap[component.getType()]) {
43-
const item = factoryMap[component.getType()](renderer, component, parent);
44-
if (ensureLayout) {
45-
if (item instanceof Text) {
46-
item.init();
47-
}
48-
item.component.ensureLayout();
49-
item.init();
50-
if (parent) {
51-
if (insertAt >= 0 && parent.container.children.length > 0 &&
52-
insertAt < parent.container.children.length) {
53-
parent.container.insertBefore(item.container, parent.container.children.item(insertAt));
54-
} else {
55-
parent.container.appendChild(item.container);
56-
}
44+
comp = factoryMap[component.getType()](renderer, component, parent);
45+
} else {
46+
// Any unknown component is effectively container
47+
comp = new Container(renderer, component, componentFactory, parent);
48+
}
49+
50+
if (ensureLayout) {
51+
if (comp instanceof Text) {
52+
comp.init();
53+
}
54+
comp.component.ensureLayout();
55+
comp.init();
56+
if (parent) {
57+
if (insertAt >= 0 && parent.container.children.length > 0 &&
58+
insertAt < parent.container.children.length) {
59+
parent.container.insertBefore(comp.container, parent.container.children.item(insertAt));
60+
} else {
61+
parent.container.appendChild(comp.container);
5762
}
5863
}
59-
return item;
6064
}
61-
throw new Error(`Cannot create component with type ${component.getType()}`);
65+
66+
return comp;
6267
};
6368

6469
// tslint:disable:max-line-length

js/apl-html/src/components/Component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ export abstract class Component<PropsType = IGenericPropType> extends EventEmitt
741741
const isLegacyComponentType: boolean = LEGACY_CLIPPING_COMPONENTS_SET.has(componentType);
742742
const isLegacyAplVersion: boolean = this.renderer && this.renderer.getLegacyClippingEnabled();
743743

744-
if (isLegacyComponentType || isParentLegacy || !isLegacyAplVersion) {
744+
if (!this.parent || isLegacyComponentType || isParentLegacy || !isLegacyAplVersion) {
745745
this.enableClipping();
746746
}
747747
}

js/apl-html/src/components/filters/Blend.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,21 @@ export interface IBlend extends IBaseFilter {
2626
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feBlend
2727
*/
2828

29+
enum BlendType {
30+
Blend = 'feBlend',
31+
Composite = 'feComposite'
32+
}
33+
2934
export function getBlendFilter(filter: Filter, imageSrcArray: string[]): IImageFilterElement | undefined {
3035
const blendId: string = uuidv4().toString();
3136
let filterImageArray: SVGFEImageElement[] = [];
32-
const blend: SVGElement = document.createElementNS(SVG_NS, 'feBlend');
33-
blend.setAttributeNS('', 'mode', getBlendMode((filter as IBlend).mode));
37+
const [elementType, operator] = getBlendMode((filter as IBlend).mode);
38+
const blend: SVGElement = document.createElementNS(SVG_NS, elementType);
39+
if (elementType === BlendType.Composite) {
40+
blend.setAttributeNS('', 'operator', operator);
41+
} else {
42+
blend.setAttributeNS('', 'mode', operator);
43+
}
3444
blend.setAttributeNS('', 'result', blendId);
3545

3646
/*
@@ -78,41 +88,47 @@ export function getBlendFilter(filter: Filter, imageSrcArray: string[]): IImageF
7888
* Return Blend Mode
7989
* https://codepen.io/yoksel/pen/BiExv
8090
*/
81-
export function getBlendMode(mode: BlendMode): string {
91+
function getBlendMode(mode: BlendMode): [BlendType, string] {
8292
switch (mode) {
8393
case BlendMode.kBlendModeNormal:
84-
return 'normal';
94+
return [BlendType.Blend, 'normal'];
8595
case BlendMode.kBlendModeMultiply:
86-
return 'multiply';
96+
return [BlendType.Blend, 'multiply'];
8797
case BlendMode.kBlendModeScreen:
88-
return 'screen';
98+
return [BlendType.Blend, 'screen'];
8999
case BlendMode.kBlendModeOverlay:
90-
return 'overlay';
100+
return [BlendType.Blend, 'overlay'];
91101
case BlendMode.kBlendModeDarken:
92-
return 'darken';
102+
return [BlendType.Blend, 'darken'];
93103
case BlendMode.kBlendModeLighten:
94-
return 'lighten';
104+
return [BlendType.Blend, 'lighten'];
95105
case BlendMode.kBlendModeColorDodge:
96-
return 'color-dodge';
106+
return [BlendType.Blend, 'color-dodge'];
97107
case BlendMode.kBlendModeColorBurn:
98-
return 'color-burn';
108+
return [BlendType.Blend, 'color-burn'];
99109
case BlendMode.kBlendModeHardLight:
100-
return 'hard-light';
110+
return [BlendType.Blend, 'hard-light'];
101111
case BlendMode.kBlendModeSoftLight:
102-
return 'soft-light';
112+
return [BlendType.Blend, 'soft-light'];
103113
case BlendMode.kBlendModeDifference:
104-
return 'difference';
114+
return [BlendType.Blend, 'difference'];
105115
case BlendMode.kBlendModeExclusion:
106-
return 'exclusion';
116+
return [BlendType.Blend, 'exclusion'];
107117
case BlendMode.kBlendModeHue:
108-
return 'hue';
118+
return [BlendType.Blend, 'hue'];
109119
case BlendMode.kBlendModeSaturation:
110-
return 'saturation';
120+
return [BlendType.Blend, 'saturation'];
111121
case BlendMode.kBlendModeColor:
112-
return 'color';
122+
return [BlendType.Blend, 'color'];
113123
case BlendMode.kBlendModeLuminosity:
114-
return 'luminosity';
124+
return [BlendType.Blend, 'luminosity'];
125+
case BlendMode.kBlendModeSourceIn:
126+
return [BlendType.Composite, 'in'];
127+
case BlendMode.kBlendModeSourceAtop:
128+
return [BlendType.Composite, 'atop'];
129+
case BlendMode.kBlendModeSourceOut:
130+
return [BlendType.Composite, 'out'];
115131
default:
116-
return 'normal';
132+
return [BlendType.Blend, 'normal'];
117133
}
118134
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict';
7+
8+
import { Filter, generateSVGFeImage, isIndexOutOfBound } from '../../utils/FilterUtils';
9+
import { SVG_NS, uuidv4 } from '../Component';
10+
import { BITMAP_IMAGE_REGEX_CHECK, IBaseFilter, IImageFilterElement } from './ImageFilter';
11+
12+
/**
13+
* @ignore
14+
*/
15+
export interface IBlur extends IBaseFilter {
16+
radius: number;
17+
source: number;
18+
}
19+
20+
/*
21+
* Apply a Gaussian blur with a specified radius. The new image is appended to the end of the array.
22+
* Specs: https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apl-filters.html#blur
23+
* Utilize svg <feGaussianBlur> filter
24+
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feGaussianBlur
25+
*/
26+
export function getBlurFilter(filter: Filter, imageSrcArray: string[]): IImageFilterElement | undefined {
27+
const blurId: string = uuidv4().toString();
28+
let filterImageArray: SVGFEImageElement[] = [];
29+
const blur: SVGElement = document.createElementNS(SVG_NS, 'feGaussianBlur');
30+
blur.setAttributeNS('', 'stdDeviation', (filter as IBlur).radius.toString());
31+
blur.setAttributeNS('', 'result', blurId);
32+
/*
33+
* All filters that operate on a single image have a default image source property of -1;
34+
* that is, by default they take as input the last image in the image array.
35+
*/
36+
let index: number = (filter as IBlur).source;
37+
38+
// Negative case : index outside source array bounds. return undefined
39+
if (isIndexOutOfBound(index, imageSrcArray.length)) {
40+
return undefined;
41+
}
42+
if (index < 0) {
43+
index += imageSrcArray.length;
44+
}
45+
const imageId: string = imageSrcArray[index];
46+
if (imageId.match(BITMAP_IMAGE_REGEX_CHECK)) {
47+
filterImageArray = generateSVGFeImage(imageId, blur);
48+
} else {
49+
blur.setAttributeNS('', 'in', imageId);
50+
}
51+
return { filterId: blurId, filterElement: blur, filterImageArray };
52+
}

0 commit comments

Comments
 (0)