2025年1月16日木曜日

Theatre.jsのStudioで編集したアニメーションをOracle Databaseに保存する

Theatre.jsではStudio UIを使ってアニメーションを編集できます。編集したアニメーションはファイルに出力しますが、これをOracle Databaseに保存するように機能を追加します。また、データベースに保存したアニメーションをStudio UIにロードする機能も組み込んでみます。データベースに保存したアニメーションのデータを読み出して、プロダクションのアニメーションを表示します。

Theatre.jsのStudio UIはブラウザの全画面を使うアプリケーションです。これは、Oracle APEXの非モーダル・ダイアログのページに実装します。非モーダル・ダイアログのページは、Oracle APEXのナビゲーションが一切生成されないため、Studio UIと競合することがありません。しかし、追加のUIを載せることもできないため、アニメーションをデータベースに保存するUIは、別ウィンドウに実装します。Studio UIの操作は、BroadcastChannelでイベントを送信して実行します。BroadcastChannelを使って非モーダル・ダイアログで処理を行う実装については、以前の記事「非モーダル・ダイアログにあるレポートをダイアログを開いたウィンドウから制御する」で紹介しています。

作成したAPEXアプリケーションは以下のように動作します。Theatre.jsのプロジェクトは、Getting StartedのWith HTML/SVGを流用しています。

ボタンExportをクリックして、その時点でのStudio UIのアニメーションをOracle Databaseに保存しています。ボタンReloadをクリックして、プロダクションのアニメーションを表示するページをリロードして初期化し、その後にボタンImportをクリックして、Oracle Databaseに保存されたアニメーションを表示しています。


このAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-theatre-js.zip

アプリケーションが依存している表EBAJ_THEATREJS_PROJECTS(アニメーションを保存する表)およびパッケージEBAJ_ASP_THEATREJS_PKGを作成するDDLを、サポートするオブジェクトインストール・スクリプトに含めているため、上記のファイルをインポートすると、アプリケーションとしては動作すると思います。

以下より作成したAPEXアプリケーションを紹介します。あくまで全画面を使うJavaScriptアプリをOracle Databaseと連携させる方法のサンプルなので、Theatre.jsとしての実用性はありません。プロジェクトについても、Getting Started With HTML/SVGの記述をファイルに直書きし、APEXのページに読み込んでいます。

最初にデータベースのオブジェクトを作成します。

EBAJ_THEATREJS_PROJECTSを作成します。

create table ebaj_theatrejs_projects (
project_id varchar2(200 char) not null
constraint ebaj_theatrejs_projects_project_id_pk primary key,
project_state clob check (project_state is json)
);
パッケージ定義EBAJ_ASP_THEATREJS_PKGを作成します。

create or replace package ebaj_asp_theatrejs_pkg as
/**
* アプリケーションSample Theatre.jsに組み込むスクリプト。
*
* 以下のDDLで作成される表EBAJ_THEATREJS_PROJECTSが操作の対象。
---
create table ebaj_theatrejs_projects (
project_id varchar2(200 char) not null
constraint ebaj_theatrejs_projects_project_id_pk primary key,
project_state clob check (project_state is json)
);
---
*/
/**
* 指定したProject IDのアニメーションをJSONドキュメントとして
* HTTPバッファに出力する。
*/
procedure ajax_restore_project_state(
p_project_id in varchar2
);
/**
* Theatre.jsのアニメーションをJSONドキュメントとして保存する。
*/
procedure ajax_save_project_state(
p_project_id in varchar2
,p_project_state in clob
);
end ebaj_asp_theatrejs_pkg;
/
パッケージ本体を作成します。

create or replace package body ebaj_asp_theatrejs_pkg as
/**
* パッケージ本体
*/
/**
* projectIdを指定してprojectStateを取り出す。
*/
procedure ajax_restore_project_state(
p_project_id in varchar2
)
as
l_project_state ebaj_theatrejs_projects.project_state%type;
l_response clob;
l_response_json json_object_t;
l_export apex_data_export.t_export;
begin
select project_state into l_project_state from ebaj_theatrejs_projects
where project_id = p_project_id;
/* レスポンスの作成 */
l_response_json := json_object_t();
l_response_json.put('status', true);
l_response_json.put('id', p_project_id);
l_response_json.put('state', l_project_state);
l_response := l_response_json.to_clob();
/* レスポンスの出力 */
l_export.mime_type := 'application/json';
l_export.as_clob := true;
l_export.content_clob := l_response;
apex_data_export.download( p_export => l_export );
exception
when no_data_found then
l_response := json_object(
'status' value false
,'reason' value SQLERRM
);
htp.p(l_response);
end ajax_restore_project_state;
/**
* projectStateを保存する。
*/
procedure ajax_save_project_state(
p_project_id in varchar2
,p_project_state in clob
)
as
l_exist number;
l_response clob;
begin
begin
select 1 into l_exist from ebaj_theatrejs_projects
where project_id = p_project_id;
/* 保存されているProject IDが無ければ例外ハンドラでINSERTする。 */
update ebaj_theatrejs_projects set project_state = p_project_state
where project_id = p_project_id;
exception
when no_data_found then
insert into ebaj_theatrejs_projects(project_id, project_state)
values(p_project_id, p_project_state);
end;
l_response := json_object(
'status' value true
,'message' value 'projectState is saved.'
);
htp.p(l_response);
exception
when others then
l_response := json_object(
'status' value false
,'reason' value SQLERRM
);
htp.p(l_response);
end ajax_save_project_state;
end ebaj_asp_theatrejs_pkg;
/
パッケージに含まれているプロシージャは、Oracle APEXのAjaxコールバックとして呼び出します。

APEXのアプリケーションを作成します。名前Sample Theatre.jsとします。

ホーム・ページにプロダクションのアニメーションを表示するリージョンと、Theatre.jsのアニメーションをデータベースに保存するボタンEXPORTと、データベースからアニメーションを取り出すボタンIMPORTを作成します。また、ページをリロードするボタンRELOADも作成します。

JavaScriptのコードは別ファイルに記述します。ページ・プロパティJavaScriptファイルURLに以下を記述します。

[module]#APP_FILES#js/production-animation-tutorial#MIN#.js


ファイルproduction-animation-tutorial.jsの内容です。

ボタンEXPORTRELOADIMPORTの処理はAPEXアクションとして実装しています。データベースからのアニメーションの取り出しは、AjaxコールバックRESTORE_PROJECT_STATEの呼び出しによって行います。

import 'https://cdn.jsdelivr.net/npm/@theatre/browser-bundles@0.7.2/dist/core-only.min.js';
/*
* プロジェクトの定義はプロダクションのページと共有するために、別ページにファンクションとして
* 定義する。
*/
import prepareProject from './project.js';
// We can now access just Theatre.core from here
const { core } = Theatre;
const projectId = apex.item("P1_PROJECT_ID").getValue();
apex.debug.info("projectId: ", projectId);
/*
* Theatre.jsのアニメーションを表示する処理。
*
* ボタンIMPORTを押した時に実行されるように、ファンクションにしている。
*/
async function restoreProjectState(projectId ) {
/*
* アニメーションを定義しているJSONをデータベースから取り出す。
*/
const response = await apex.server.process(
"RESTORE_PROJECT_STATE",
{
pageItems: "#P1_PROJECT_ID"
}
);
const projectState = JSON.parse(response.state);
apex.debug.info("projectState: ", projectState);
/*
* アニメーションを実行する。
*/
const r = prepareProject(core, projectId, projectState);
const project = r.project;
const sheet = r.sheet;
// wait for project to be ready
project.ready.then(() => {
apex.debug.info('project is ready', project);
sheet.sequence.play({ iterationCount: Infinity })
});
};
/*
* Theatre.jsのアニメーションのエクスポートとインポートを行う。
*/
const channel = new BroadcastChannel('control-theatre');
// エクスポートの成功をP1_STATUSに表示する。
channel.addEventListener("message", (event) => {
apex.debug.info("status: ", event);
if ( event.data.type === 'status' ) {
apex.item("P1_STATUS").setValue(event.data.message);
}
});
const controlElement = document.getElementById("CONTROLS");
const controls = apex.actions.createContext("controls", controlElement);
controls.add([
{
// EXPORTはStudioが実装されているページで処理する。
name: "EXPORT",
action: (event, element, args) => {
channel.postMessage( { type: "export" } );
}
},
{
// ページをリロードしてアニメーションを初期化する。
name: "RELOAD",
action: (event, element, args) => {
window.location.reload();
apex.item("P1_STATUS").setValue(null);
}
},
{
// IMPORTはこのページで処理する。
name: "IMPORT",
action: (event, element, args) => {
restoreProjectState( projectId );
apex.item("P1_STATUS").setValue("projectState is restored.");
}
}
]);
インポートしているファイルproject.jsでは、Theatre.jsのプロジェクトを記述しています。このファイルはStudio UIを実装しているJavaScriptファイルにも読み込みます。

/*
* 以下はTheatre.jsのGetting StartedのWith HTML/SVGのコード
* https://www.theatrejs.com/docs/latest/getting-started/with-html-svg
*/
export default function prepareProject( core, projectId, projectState ) {
const project = core.getProject( projectId, {
state: projectState
});
const sheet = project.sheet('Sheet 1');
const obj = sheet.object('Heading 1', {
y: 0,
opacity: core.types.number(1, { range: [0, 1] }),
});
const articleHeadingElement = document.getElementById('article-heading');
// animations
obj.onValuesChange((obj) => {
articleHeadingElement.style.transform = `translateY(${obj.y}px)`
articleHeadingElement.style.opacity = obj.opacity
});
return { project: project, sheet: sheet, obj: obj };
}
view raw project.js hosted with ❤ by GitHub
AjaxコールバックRESTORE_PROJECT_STATEでは、以下のコードを実行します。
ebaj_asp_theatrejs_pkg.ajax_restore_project_state(
    p_project_id => :P1_PROJECT_ID
);

プロダクションのアニメーションを表示する領域として、静的コンテンツのリージョンTheatre.jsを作成します。

ソースHTMLコードにアニメーションを適用するH1要素を記述します。

<h1 id="article-heading" style="text-align: center">Welcome</h1>

外観テンプレート・オプションにより、リージョンの高さ320pxにしています。ボタンを配置しているリージョンをリージョンの上に移動し、リージョンの高さをもう少し高くしても良いかもしれません。


ページ・アイテムP1_PROJECT_IDプロジェクトIDを設定します。ソースタイプにアイテムアイテムとしてG_PROJECT_IDを設定しています。つまり、このページ・アイテムの値はアプリケーション・アイテムG_PROJECT_IDに設定した値になります。

このようにしている理由は、ページ・アイテムの値はJavaScriptのコードから簡単に参照できるためです。Stuido UIのページでも同じプロジェクト名を参照しますが、ページが異なるためJavaScriptのコードからはP1_PROJECT_IDの値は参照できません。そのため、Studio UIを実装したページでは、ページ・アイテムとしてP2_PROJECT_IDを作成し、そのソースをアプリケーション・アイテムG_PROJECT_IDにしています。結果として、双方のページで、アプリケーション・アイテムG_PROJECT_IDの値をプロジェクト名として参照します。


共有コンポーネントアプリケーション・アイテムとしてG_PROJECT_IDを作成します。


アプリケーション・アイテムへの値の設定は、アプリケーションの計算により行います。

頻度計算ポイントとして新規インスタンス(新規セッション)開始時を選択し、計算タイプとして静的割り当て計算HTML Animation Tutorialを設定します。

Theatre.jsのプロジェクトがファイルproject.jsに直書きされていることや、アプリケーション・アイテムに設定しているプロジェクト名が決め打ちなので、このアプリケーション自体はTheatre.jsのGetting StartedのWith HTML/SVGで説明されている以上のことはできません。


ボタンをクリックして実行した処理のステータスを表示するページ・アイテムとしてP1_STATUSを作成します。


ボタンを配置する静的コンテンツのリージョンを作成します。外観テンプレートButtons Containerを選択します。APEXアクションのコンテキストを作成する際にJavaScriptからリージョンを選択できるよう、詳細静的IDとしてCONTROLSを設定します。


ボタンについては、それぞれAPEXアクションを呼び出すように、動作アクションとして動的アクションで定義を選択し、詳細カスタム属性としてdata-action="EXPORT"data-action="RELOAD"data-action="IMPORT"を設定します。


プロダクションのアニメーションの表示と、それをコントロールするページの実装は以上で完了です。

Theatre.jsのStudio UIは、外観ページ・モード非モーダル・ダイアログとしたページに実装します。

ページ・プロパティJavaScriptファイルURLとして以下を記述します。

[module]#APP_FILES#js/theatre-studio#MIN#.js


インポートするtheatre-studio.jsの内容です。

import 'https://cdn.jsdelivr.net/npm/@theatre/browser-bundles@0.7.2/dist/core-and-studio.js';
/*
* プロジェクトはプロダクションのページと共有するため、別ページにファンクションとして
* 定義する。
*/
import prepareProject from './project.js';
// We can now access Theatre.core and Theatre.studio from here
const { core, studio } = Theatre;
/*
* プロジェクトIDは、アプリケーション・アイテムG_PROJECT_IDに定義されている。
*/
const projectId = apex.item("P2_PROJECT_ID").getValue();
apex.debug.info("projectId: ", projectId);
/*
* データベースに保存されているアニメーションを取り出して、
* それでStudioを初期化する。
*/
apex.server.process(
"RESTORE_PROJECT_STATE",
{
pageItems: "#P2_PROJECT_ID"
},
{
success: (data) => {
const projectState = JSON.parse(data.state);
/*
* Theatre.jsのStudioを開始する。
*/
studio.initialize(); // Start the Theatre.js UI
/*
* Theatre.jsのStudioの起動時は、アニメーションのデータをlocalStorageから
* 取り出す。DBから取り出したprojectStateを与えると、Studioは
* ブラウザとディスクのデータでConflictがあると報告する。
* ブラウザとディスクのどちらを使うか選択できるので、DBのデータを使う場合は
* ディスク、localStorageを使う場合はブラウザを選択する。
*/
apex.debug.info("initial projectStage: ", projectState);
const r = prepareProject(core, projectId, projectState);
/*
* アニメーションはブラウザのlocalStorageから取り出すので、project自体は
* 初期化以後、参照することがない。
*/
}
}
)
/*
* BroadcastChannelからイベントを受け取って、アニメーションをエクスポートする。
* 本サンプルで追加したデータベースへアニメーションを保存する処理。
*/
const channel = new BroadcastChannel('control-theatre');
channel.addEventListener("message", (event) => {
apex.debug.info("event: ", event);
if (event.data.type === "export") {
/*
* 現在のアニメーションの設定をJSONドキュメントとして取り出す。
* サーバーに送信するためにページ・アイテムP2_PROJECT_STATEに
* 設定する。
*/
const projectState = studio.createContentOfSaveFile(projectId);
apex.debug.info("projectState: ", projectState);
apex.item("P2_PROJECT_STATE").setValue(JSON.stringify(projectState));
/*
* アニメーションをデータベースに保存する。
*/
apex.server.process(
"SAVE_PROJECT_STATE",
{
pageItems: "#P2_PROJECT_ID,#P2_PROJECT_STATE"
},
{
success: (data) => {
apex.debug.info(data);
channel.postMessage( { type: "status", message: data.message } );
}
}
)
}
});

データベースからアニメーションを取り出すためにAjaxコールバックRESTORE_PROJECT_STATEを呼び出し、保存するためにSAVE_PROJECT_STATEを呼び出します。RESTORE_PROJECT_STATEは先のページに実装したものと同じ処理ですが、ページが異なるため、同じAjaxコールバックを作成する必要があります。

AjaxコールバックSAVE_PROJECT_STATEでは、以下のコードを実行します。
ebaj_asp_theatrejs_pkg.ajax_save_project_state(
    p_project_id => :P2_PROJECT_ID
    ,p_project_state => :P2_PROJECT_STATE
);

ページ・アイテムP2_PROJECT_IDは、先ほどのP1_PROJECT_IDと同様に、ソースアイテムとしてG_PROJECT_IDを設定します。


ページ・アイテムP2_PROJECT_STATEには、アニメーションの定義であるJSONドキュメントを設定します。セッション・ステートデータ型としてCLOBを選択します。また、この値はJavaScriptから設定するため、設定保護された値オフにします。


静的コンテンツのリージョンを作成し、ソースHTMLコードにアニメーションの対象とするH1要素を記述します。

<h1 id="article-heading" style="text-align: center">Welcome</h1>


以上でStudio UIの実装も完了です。

Studio UIはナビゲーション・メニューから開きます。ページ作成ウィザードナビゲーション・メニューの作成オンにしていると、自動的にダイアログを開くメニュー・エントリが作成されます。


アプリケーションの背景を黒にするため、テーマ・ローラーを開き、テーマとしてVita - Darkを選択しています。


作成したアプリケーションの紹介は以上になります。

ブラウザとディスクでアニメーションに差異がある場合の、Studio UIの画面です。


Oracle APEXのアプリケーション作成の参考になれば幸いです。