Awesome
NOTICE for MM >2.25 USERS
Use MMM-Scenes2
instead of this module from MM 2.25.
MMM-Scenes
“Life is a theatre set in which there are but few practicable entrances.”
― Victor Hugo, Les Misérables
MagicMirror module to change screen scenes by time and order with ANIMATION EFFECT.
Demo
TOC
- MMM-Scenes
Concept
The scenario of the MM screen is made up of a series of scenes. Each module has its role in its appearance scenes to enter and exit by design.
When a scene begins, all modules whose roles end, will be expelled, and all modules that have the parts in that scene will be admitted.
As described in the scenario, your MM screen will play a drama with modules.
Features
- control show/hide modules by assigning scene names into the module's class
- custom animations for modules expel/admit
- control scenes by notification and WebURL endpoints.
- Loop control
- custom indicators
Install
cd ~/MagicMirror/modules
git clone https://github.com/MMRIZE/MMM-Scenes
cd MMM-Scenes
npm install
Config
{
module: "MMM-Scenes",
position: 'bottom_right', // Position of indicator
classes: "scene1 scene2 scene3", // Even indicator needs the direction when it is presented.
hiddenOnStartup: true,
config: {
scenario: [ "scene1", "scene2", "scene3"],
// other options...
}
}
You can find an example in th examples
directory.
Global options
These options will apply to each scene unless defined in the scene newly. All the properties are omittable, and if omitted, a default value will be used.
property | default | description |
---|---|---|
scenario | [] | The order-set of scenes. You can set the scene name(class name) or scene definition (object) as the items of this property. |
autoLoop | "infinity" | Looping scenes in scenario . <br> Available values: "no" , "once" , "infinity" <br> Default Value: "infinity" |
startScene | '' | (optional) If set, this scene will be displayed on startup. If omitted, the first scene of scenario will be displayed at first. |
duration | 10000 | (ms) When the next scene will come after the current scene starts. On autoLoop = no , this will be ignored. |
admitAnimation | "default" | When the scene starts, how the modules will appear. See animation |
admitDuration | 1000 | (ms) speed of admitAnimation |
admitGap | 0 | (ms) the entrance gap of modules in admitAnimation <br> In admitGap = 0 all roled modules will enter together, in admitGap = 500 each roled module will enter sequentially with 500ms of delay. |
expelAnimation | "default" | Before the new scene starts, how unrolled modules will be expelled. See animation |
expelDuration | 1000 | (ms) speed of expelAnimation |
expelGap | 0 | (ms) the exit gap of modules in expelAnimation |
lockString | "mmm-scenes" | Lock-key of MM's locking mechanism. Generally, you don't need to touch. |
inactiveIndicators | ['○'] | Array of inactive scene indicators See indicators |
activeIndicators | ['●'] | Array of active scene indicators. See indicators |
Assign scene to modules
Usually, A scene is defined as text. If you want some module to admit into the specific scene, give the scene name as the module's class name.
{
module: "clock",
position: "top_left",
classes: "scene1 scene3"
},
{
module: "helloworld",
position: "top_right",
classes: "scene2"
}
In this example, the clock
module will appear in scene1
and scene3
and will be expelled from scene2
or scene4
. helloworld
module will appear only in scene2
.
Describe scenes in scenario
In the module's configuration, you can describe scenario
with which scenes will appear by order.
config: {
scenario: ["scene1", "scene2", "scene1", "scene3", "scene1", "scene4"],
}
All the scenes have the same global properties by default. But you can specify a scene with scene definitions in scenario
.
config: {
duration: 10 * 60 * 1000,
scenario: [
"scene1",
"scene2",
{
name: "scene3",
duration: 30 * 60 * 1000,
admitAnimation: "jelly"
},
"scene4"
],
}
In this example, scene1
, scene2
and scene4
will have 10 * 60 * 1000
as duration
and default animations, but scene3
will have 30 * 60 * 1000
with jelly
style admitAnimation.
scene object (scene3)
should have name
property. Other available properties are admitAnimation
, admitDuration
, admitGap
, expelAnimation
, expelDuration
, expelGap
and duration
Animation
admitAnimation
and expelAnimation
would point which animation effect would be applied to each scene transition.
They could have three types of values as a definition of animation.
- predefined animation name
Some predefined animation behaviours are prepared in animation.mjs
file. You can select one of them for your animation effect.
expelAnimation: "jelly",
admitAnimation: "pageDown"
- keyframe array or object
Or you can set a keyframe
array or object for your own custom animation. (See reference)
expelAnimation: [ // keyframe array
{ opacity: 1, transform: 'scale(1, 1)' },
{ opacity: 0.5, transform: 'scale(0.5, 1)' },
{ opacity: 0, transform: 'scale(0, 0)' }
],
admitAnimation: { // keyframe object
opacity: [ 0, 0.9, 1 ],
offset: [ 0, 0.8 ],
easing: [ 'ease-in', 'ease-out' ],
},
- custom animation function
Or you can assign your custom animation function to control more detailly. The animation function should return Promise
or be async function
to know when the animation finishes.
expelAnimation: async ({ moduleWrapper, duration }) => {
const asleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
moduleWrapper.style.transition = 'opacity ' + duration / 1000 + 's'
moduleWrapper.style.opacity = 0
await asleep(duration)
return
},
admitAnimation: ({ moduleWrapper, duration, module }) => {
const position = (pos) => {
if (pos.search("_left") >= 0) return "(-50vw, 0)";
if (pos.search("_right") >= 0) return "(50vw, 0)";
if (pos.search("top_bar") >= 0 || pos.search("upper") >= 0 || pos.search("middle") >= 0) return "(0, 50vh)";
if (pos.search("bottom_bar") >= 0 || pos.search("lower") >= 0) return "(0, 50vh)";
return "(0,0)";
};
const value = position(module.data.position)
let keyframe = [
{ transform: 'translate(0, 0)', opacity: 1, easing: 'ease-out' },
{ transform: `translate${value}`, opacity: 0, easing: 'ease-in' },
];
return new Promise((resolve, reject) => {
moduleWrapper.animate(keyframe, { duration }).onfinish = resolve
})
},
You can also add your custom animation function into animation.mjs
file, then use it's name simply.
An animation function will get DefinitionObject { moduleWrapper, module, duration, isExpel }
argument. Return value has to be a Promise
.
animation = async (DefinitionObject) => {
const {
moduleWrapper, // HTML Dom of target module
module, // MM module itself
duration, // Animation duration
isExpel, // Only valid in 'animation.mjs'
} = DefinitionObject
console.log(module.name) // do your stuff
...
return
}
Notifications & WebAPI endpoints
Incoming notifications
- Each incoming notification could have a
callback
function as a member of the payload. It will be called when your notification requesting is done, andadmitAnimation
is finished.
this.sendNotification('SCENE_NEXT', {
callback: (response) => { console.log(response.ok) }
})
- You can override animation for this instant scene changing with
payload.options
.
this.sendNotification('SCENE_ACT', {
index: 1,
options: {
duration: 30 * 60 * 1000,
admitAnimation: 'jelly', // or keyframe or custom animation function
admitDuration: 2000,
}
})
SCENES_NEXT
, payload: { callback, options }
Play next scene. If the current scene is not inside the scenario
, the next scene will be the first scene(index: 0).
SCENES_PREV
, payload: { callback, options }
Play previous scene. If the current scene is not inside of scenario
, the previous scene doesn't exist.
SCENES_ACT
, payload: { callback, options, name, index }
Play specific scene.
-
With the
name
payload, It will show the modules which have that name as a class. If not exists, no module will appear. (Empty screen) -
With the
index
payload, It will show the Nth module of the scenario. If with the wrong index, nothing will happen. The index is zero-based. (index:0 => first scene, index:1 => second scene) -
If
name
andindex
are used together simultaneously,name
is primary toindex
.
SCENES_CURRENT
, payload: { callback }
Ask about the current scene. The callback
function will get a data object as an argument.
this.sendNotification('SCENES_CURRENT' { callback: (info) => {
console.log(info)
/*
`info` will have something like these;
{
scenario: [...],
nextIndex,
prevIndex,
index,
name,
duration,
options: { ... },
...
}
*/
}})
Outgoing notification
SCENES_CHANGED
, payload: { name, index, duration, scenario, autoLoop }
When a scene is changed, this notification will be emitted.
WebAPI endpoints
You can access MM url to control this module from outside of MM. e.g.) IFTTT.
GET /scenes/next
Play next scene.
GET /scenes/prev
Play previous scene
GET /scenes/act/:indexNumber
Play specific scene by given index
http://localhost/scenes/act/1
GET /scenes/act/:sceneName
Play specific scene by given name
http://localhost/scenes/act/scene2
TelegramBot integration
You can control MMM-Scenes using the Telegram app by installing the MMM-TelegramBot module and adding MMM-TelegramBot configuration to your scenes.
Telegram usage
Once installed and configured, you can control your MMM-Scenes display by sending messages in the Telegram app to your previously created Telegram Bot. The supported commands are as follows:
- /scene info
- /scene next
- /scene prev
- /scene name:scenename
- /scene_index <number>
For example, to play the scene named 'scene1' in the scenario, issue the command:
/scene name:scene1
To play scene index 2 in the scenario, issue the command:
/scene_index 2
Indicators
inactiveIndicators: ['○'],
activeIndicators: ['●'],
inactiveIndicators: ['①', '②', '③'],
activeIndicators: ['❶', '❷', '❸']
inactiveIndicators: [''],
activeIndicators: ['Today', 'Family', 'Ent.'],
inactiveIndicators: ['1', '2', '3'],
activeIndicators: ['1', '2', '3'],
-
When the current scene does not belong to
scenario
, thescene name
will be displayed instead of the scene indicator -
When the number of indicators is less than that of
scenario
, the last item will be applied to the rest. (the first example or the third example.) -
You can decorate the indicators with CSS in your
custom.css
; The HTML created will be like this
<div class="scenes_indicator">
<span class="scenes_indicator_scene index_0 inactive first">○</span>
<span class="scenes_indicator_scene index_1 active">●</span> <!-- current scene -->
<span class="scenes_indicator_scene index_2 inactive last">○</span>
<div>
/* Just an example */
.scenes_indicator_scene {
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
.scenes_indicator_scene.inactive {
color: #999;
}
.scenes_indicator_scene.active {
color: orange;
font-weight: bold;
}
.scenes_indicator_scene.first::before {
content: "[ "
}
.scenes_indicator_scene.last::after {
content: " ]"
}
Predefined animation
- See animations.mjs.origin. It will be copied to
animations.mjs
. You can add your custom animation definition in that file. (PR will be welcomed for more animations.)
Info
Memo
- RPI3 or older/weaker SBC doesn't have enough power to handle the animation. In that case, just use animation
default
or avoid serious effects.
History
1.1.0 (2021-11-01)
- ADDED :
MMM-TelegramBot
commandable. (Thanks to @doctorfree)
1.0.0 (2021-10-12)
- released
Author
- Seongnoh Yi (eouia0819@gmail.com)