Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CodenameOne/src/com/codename1/ui/util/Resources.java
Original file line number Diff line number Diff line change
Expand Up @@ -930,8 +930,15 @@ public static void registerGeneratedImage(String id, Image image) {
}
synchronized (generatedImages) {
generatedImages.put(id, image);
if (id.endsWith(".svg")) {
generatedImages.put(id.substring(0, id.length() - 4), image);
// Register the bare stem too so getImage("home") works whether
// the source asset was home.svg, home.json (Lottie), or
// home.lottie. Keeps the call site format-agnostic.
int dot = id.lastIndexOf('.');
if (dot > 0) {
String stem = id.substring(0, dot);
if (!generatedImages.containsKey(stem)) {
generatedImages.put(stem, image);
}
}
}
}
Expand Down
104 changes: 94 additions & 10 deletions docs/developer-guide/SVG-Transcoder.asciidoc
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
= Build-Time SVG Images
= Build-Time Vector & Animation Images
:source-highlighter: highlight.js

The build-time SVG transcoder lets you author UI icons and
illustrations as SVG and have them rendered as native Codename One
`Image` instances on every platform (iOS, Android, JavaSE simulator,
JavaScript) without shipping a runtime SVG parser.
The build-time vector transcoder lets you author UI icons and
illustrations as SVG or Lottie / Bodymovin JSON and have them rendered
as native Codename One `Image` instances on every platform (iOS,
Android, JavaSE simulator, JavaScript) without shipping a runtime SVG
or Lottie parser.

Both formats share the same pipeline: each source file is lowered into
the SVG transcoder's model, the same `JavaCodeGenerator` emits a
`com.codename1.ui.GeneratedSVGImage` subclass, and the same
`SVGRegistry` makes the result available via
`Resources.getImage("name.<ext>")`. The rest of this guide therefore
covers SVG in detail; Lottie gets its own section at the end that only
calls out the parts that differ.

== Motivation

Expand Down Expand Up @@ -202,15 +211,90 @@ with `Timeline`, you must register the image with a `Form`'s animation
manager (or set it as a `Component.setIcon` with `isAnimation()` true)
for the repaint loop to tick the SMIL clock.

== Lottie animations

Lottie / Bodymovin JSON files are picked up by the same `transcode-svg`
goal. Drop them next to your CSS (or under `src/main/lottie/`) and the
Lottie parser lowers each one into the SVG model -- so everything in
the previous sections (sizing keys, registry, theme `url(...)` lookup,
`Resources.getImage(...)`, the per-port wiring) applies unchanged:

[source]
----
src/main/css/
theme.css
spinner.json <-- Lottie/Bodymovin export
----

[source,css]
----
SpinnerStyle {
background: url(spinner.json);
cn1-svg-width: 12mm;
cn1-svg-height: 12mm;
bg-type: image_scaled_fit;
}
----

[source,java]
----
Image spin = Resources.getGlobalResources().getImage("spinner.json");
// or by stem, like a multi-image:
Image spin2 = Resources.getGlobalResources().getImage("spinner");
----

`.lottie` (dotLottie ZIP) files are accepted alongside `.json` for
forward compatibility, but the archive container isn't yet extracted
-- export your animation as a plain Bodymovin JSON for now.

=== Lottie feature coverage

The parser targets the subset of Bodymovin a "spinner" or "pulse"
animation typically uses. Anything outside the subset is dropped
without warning so a file with mixed coverage still produces a renderable
class:

|===
| Feature | Status

| Shape layers (`ty:4`) with grouped `rc` / `el` / `sh` primitives | Full
| Solid color layers (`ty:1`) | Rendered as a filled rect
| Shape fills (`fl`) and strokes (`st`) -- solid colors | Full
| Layer transform (anchor, position, scale, rotation, opacity) -- static | Full
| Animated rotation / position / scale -- 2 keyframes | Full (loops indefinitely over the comp duration)
| Animated colors / opacity | Collapsed to the first keyframe
| Bezier easing on keyframes | Linear interpolation (easing curves ignored)
| Multi-keyframe properties (3+) | Collapsed to first vs. last (matches the SVG codegen's `from`/`to` model)
| Trim path (`tm`), repeater (`rp`), merge (`mm`), rounded corners (`rd`) | Ignored
| Gradient fills (`gf`) / gradient strokes (`gs`) | Ignored
| Text layers, image layers, precomp, mattes, expressions | Ignored
|===

For animations that need higher fidelity than the subset above, export
the relevant frame as an SVG and use the SVG transcoder path directly
-- the runtime classes are identical.

== Troubleshooting

`Resources.getImage("name.svg")` returns null or a 1×1 transparent PNG::
The transcoder didn't run, or it ran with zero SVGs. Confirm the SVG
lives under `src/main/css/` (or `src/main/svg/`) and that the
`transcode-svg` goal is bound in the project POM. For arbitrary
Resources bundles loaded outside the global slot, call
The transcoder didn't run, or it ran with zero source files. Confirm
the asset lives under `src/main/css/` (or `src/main/svg/` /
`src/main/lottie/` for the dedicated dirs) and that the
`transcode-svg` goal is bound in the project POM. The same goal
handles `.svg`, `.json`, and `.lottie` -- one goal, both formats.
For arbitrary Resources bundles loaded outside the global slot, call
`com.codename1.ui.util.Resources.registerGeneratedImage(name, image)`
yourself with a fresh `new com.codename1.generated.svg.YourSvg(widthMm, heightMm)`.
yourself with a fresh
`new com.codename1.generated.svg.YourAsset(widthMm, heightMm)`.

Lottie animation looks frozen or starts halfway through::
The Lottie parser collapses each animated property's keyframe array
to its first and last value, then loops indefinitely over the
composition's duration. Animations with three or more keyframes on
the same property therefore play as a straight first-to-last
interpolation. Re-export the comp split into two-keyframe segments
or use an SVG/SMIL export for that animation if you need exact
keyframe playback.

SVG looks the wrong size::
Switch the rule to `cn1-svg-width` / `cn1-svg-height` in millimeters.
Expand Down
5 changes: 5 additions & 0 deletions maven/codenameone-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
<artifactId>codenameone-svg-transcoder</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>codenameone-lottie-transcoder</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.codename1.maven;

import com.codename1.lottie.transcoder.LottieTranscoder;
import com.codename1.svg.transcoder.SVGTranscoder;
import com.codename1.svg.transcoder.SVGTranscoder.GeneratedClass;

Expand Down Expand Up @@ -33,17 +34,25 @@
import java.util.regex.Pattern;

/**
* Scans an application module for SVG files, transcodes each into a
* Scans an application module for vector animation files (SVG and
* Lottie / Bodymovin JSON), transcodes each into a
* {@code com.codename1.ui.GeneratedSVGImage} subclass under
* {@code target/generated-sources/svg}, and emits a registry class that
* installs each transcoded image into a Resources instance (and the global
* fallback) under its source filename.
*
* <h3>Source layout</h3>
* SVG files are picked up from both {@code src/main/css/} and
* {@code src/main/svg/}. Drop your SVGs next to the CSS file that references
* them; the mojo finds them either way. Theme CSS keeps the natural
* {@code background: url(spinner.svg);} reference.
* Files are picked up from format-specific directories plus the shared
* {@code src/main/css/} directory so designers can drop assets next to the
* theme CSS that references them:
* <ul>
* <li>{@code src/main/svg/} -- {@code *.svg}</li>
* <li>{@code src/main/lottie/} -- {@code *.json}, {@code *.lottie}</li>
* <li>{@code src/main/css/} -- either of the above</li>
* </ul>
* Theme CSS keeps the natural {@code background: url(spinner.svg);}
* reference for SVG, and the same {@code url(...)} syntax resolves
* {@code .json} / {@code .lottie} at runtime.
*
* <h3>CSS hints</h3>
* For each {@code url(*.svg)} occurrence the mojo also looks at the rule's
Expand All @@ -67,7 +76,31 @@
requiresDependencyCollection = ResolutionScope.NONE)
public class TranscodeSVGMojo extends AbstractCN1Mojo {

private static final String[] DEFAULT_SVG_DIRS = { "src/main/svg", "src/main/css" };
private static final String[] DEFAULT_SVG_DIRS = {
"src/main/svg",
"src/main/lottie",
"src/main/css"
};

/** Recognized vector source extensions plus the format key the file
* parses as. Order matters only when multiple extensions could match
* the same bytes -- they cannot here. */
private enum VectorFormat {
SVG(".svg"),
LOTTIE_JSON(".json"),
LOTTIE_PACK(".lottie");

final String ext;
VectorFormat(String ext) { this.ext = ext; }

static VectorFormat fromFilename(String name) {
String lower = name.toLowerCase();
for (VectorFormat f : values()) {
if (lower.endsWith(f.ext)) return f;
}
return null;
}
}

private static final String DEFAULT_PACKAGE = "com.codename1.generated.svg";

Expand Down Expand Up @@ -136,15 +169,22 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException
packageDir.mkdirs();
for (File svg : svgs) {
String resourceName = svg.getName();
VectorFormat fmt = VectorFormat.fromFilename(resourceName);
if (fmt == null) {
// locateSvgs() already filtered to recognized extensions;
// defensive guard for future format additions.
continue;
}
String className = uniqueClassName(SVGTranscoder.classNameFor(resourceName), usedClassNames);
usedClassNames.add(className);
File outFile = new File(packageDir, className + ".java");
if (outFile.exists() && outFile.lastModified() >= svg.lastModified()) {
getLog().debug("SVG transcoder up-to-date for " + svg.getName());
getLog().debug("Vector transcoder up-to-date for " + svg.getName());
} else {
getLog().info("Transcoding SVG " + svg.getName() + " -> " + className + ".java");
getLog().info("Transcoding " + fmt.name() + " " + svg.getName()
+ " -> " + className + ".java");
try {
SVGTranscoder.transcode(svg, svgPackage, className, outFile);
transcodeByFormat(fmt, svg, svgPackage, className, outFile);
} catch (IOException ex) {
throw new MojoExecutionException("Failed to transcode " + svg, ex);
}
Expand Down Expand Up @@ -200,9 +240,12 @@ private static final class CssHint {
float heightMm;
}

/** Scans theme CSS files for {@code url(*.svg)} together with the
* enclosing rule's {@code cn1-source-dpi} / {@code cn1-svg-width} /
* {@code cn1-svg-height}. Returns a map of svgFilename -> CssHint. */
/** Scans theme CSS files for {@code url(*.svg|*.json|*.lottie)}
* together with the enclosing rule's {@code cn1-source-dpi} /
* {@code cn1-svg-width} / {@code cn1-svg-height}. Returns a map of
* filename -> CssHint. The CSS hint vocabulary is the SVG transcoder's
* -- the same {@code cn1-svg-width} property sizes Lottie outputs too
* because both share the {@code GeneratedSVGImage} base. */
private Map<String, CssHint> scanCssHints() throws MojoExecutionException {
Map<String, CssHint> result = new HashMap<String, CssHint>();
File cssDir = new File(project.getBasedir(), "src/main/css");
Expand All @@ -213,7 +256,7 @@ private Map<String, CssHint> scanCssHints() throws MojoExecutionException {
collectCss(cssDir, cssFiles);
Pattern blockPattern = Pattern.compile("\\{([^}]*)\\}", Pattern.DOTALL);
Pattern svgUrlPattern = Pattern.compile(
"url\\(\\s*['\"]?\\s*([^'\")\\s]+?\\.svg)\\s*['\"]?\\s*\\)",
"url\\(\\s*['\"]?\\s*([^'\")\\s]+?\\.(?:svg|json|lottie))\\s*['\"]?\\s*\\)",
Pattern.CASE_INSENSITIVE);
Pattern dpiPattern = Pattern.compile(
"cn1-source-dpi\\s*:\\s*([\\w-]+)\\s*;?",
Expand Down Expand Up @@ -365,12 +408,28 @@ private static void collect(File dir, List<File> out) {
for (File f : entries) {
if (f.isDirectory()) {
collect(f, out);
} else if (f.getName().toLowerCase().endsWith(".svg")) {
} else if (VectorFormat.fromFilename(f.getName()) != null) {
out.add(f);
}
}
}

private static void transcodeByFormat(VectorFormat fmt, File src,
String pkg, String className, File outFile) throws IOException {
switch (fmt) {
case SVG:
SVGTranscoder.transcode(src, pkg, className, outFile);
break;
case LOTTIE_JSON:
case LOTTIE_PACK:
// .lottie (dotLottie ZIP) needs an extra archive-extract step
// we don't perform here yet -- the parser treats the bytes as
// a JSON document. Drop a plain Lottie JSON for now.
LottieTranscoder.transcode(src, pkg, className, outFile);
break;
}
}

private static void collectCss(File dir, List<File> out) {
File[] entries = dir.listFiles();
if (entries == null) {
Expand Down
55 changes: 55 additions & 0 deletions maven/lottie-transcoder/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>com.codenameone</groupId>
<artifactId>codenameone</artifactId>
<version>8.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>codenameone-lottie-transcoder</artifactId>
<version>8.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>codenameone-lottie-transcoder</name>
<description>
Build-time tool that parses Lottie/Bodymovin JSON animations and
emits Codename One Image subclasses that render them via the
Graphics API. Mirrors the SVG transcoder pipeline. Supports a
useful subset of shape layers (rectangles, ellipses, paths) with
fill/stroke and keyframe-animated transforms.
</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>com.codenameone</groupId>
<artifactId>codenameone-svg-transcoder</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading
Loading