Sprite sometimes doesn't load in Phaser game - reactjs

Sometimes the game starts without waiting for all the assets to load. This happens most often with the spaceStation which is the largest sprite at 3372x700 px. When the game first boots, the spaceStation isn't loaded. But if you leave the game then come back to it without refreshing the page, it will be loaded the second time. I am using the Phaser game engine with the IonPhaser wrapper for React.
The game is hosted here so you can see what I mean: https://seanhetzel.github.io/star-runner/#/
Here is my code for the spaceStation sprite:
import spaceStation from "../assets/space-station-sprite-sheet.png";
// other stuff...
preload: function() {
// other stuff...
const spaceStationImg = new Image();
spaceStationImg.onload = () => {
this.textures.addSpriteSheet("spaceStation", spaceStationImg, {
frameWidth: 3372,
frameHeight: 700
});
};
spaceStationImg.src = spaceStation;
// other stuff...
}
// other stuff...
create: function() {
// other stuff...
const config = {
key: "lights",
frames: this.anims.generateFrameNumbers("spaceStation", {
start: 0,
end: 6
}),
frameRate: 3,
repeat: -1
};
this.anims.create(config);
this.spaceStation = this.impact.add
.sprite(1686, 350, "spaceStation")
.play("lights")
.setDepth(1);
// other stuff...
}
Am I not loading it correctly?
Thanks!

The Phaser 3 Loader should use the imported sprite instead of pointing directly to the relative path in the preload() function when using react.
import spaceStation from "../assets/space-station-sprite-sheet.png";
// other stuff...
this.load.spritesheet(
'spaceStation',
spaceStation, // <-- not '../assets/space-station-sprite-sheet.png'
{
frameWidth:3372,
frameHeight:700
}
);
Now it’s getting the sprite from import spaceStation from "../assets/space-station-sprite-sheet.png"; import instead of directly pointing to the relative path and using the Phaser 3 Loader.

Related

Three js change color on press button removes the details of the model

Hope you all are doing best in you life!
I've newly intraction with 3d work. I'm making the 3d virtual showroom you can say 3d commerce page,where i need to put 3d Models. I want to add functionality of changing models color (limited colors) by users. like this: [https://codesandbox.io/s/v1fgk].(I'm using three js not react three fiber.)
THREE JS Version: "three": "^0.135.0",
The problem is that:
1) When i change the color of the mesh the details of the model are deleted.what can i do for this?
2) I face alot of lighting issues during this work.finally I'm using HDRI envornment instead of lighting in my models.
am i doing best?
Actual Model:
On Change Color:
Why my models's details are going to delete?
On Changing Color i'm doing this:
// const loader = new THREE.TextureLoader();
// const aoTexture = loader.load('textures/BottleModel/Model2/Material.002_Mixed_AO.png')
// const displacementTexture = loader.load('textures/BottleModel/Model2/base_Height.png')
// const metalicTexture = loader.load('textures/BottleModel/Model2/Material.002_Metallic.png')
// const roughnessTexture = loader.load('textures/BottleModel/Model2/Material.002_Roughness.png')
const BottleMesh=model.current.children.find((item)=>item.name==="cup_base")
BottleMesh.children.map((item)=>{
if(item.name==="Cylinder"){
item.material = new THREE.MeshStandardMaterial({
color: code,
// aoMap:aoTexture,
// metalnessMap : metalicTexture,
// displacementMap:displacementTexture,
// roughnessMap:roughnessTexture
})
}
})
CREATING MODEL AND ADDING ENVORNMENT:
export const createModel = (model, scene, path, setLoading,renderer) => { //model is a react ref, remember that
setLoading(true)
const dracoLoader = new DRACOLoader();
// // Specify path to a folder containing WASM/JS decoding libraries.
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
let loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
let generator = new THREE.PMREMGenerator(renderer);
new RGBELoader().setPath('/textures/hdri/') .load('dancing_hall_4k.hdr', function (texture) {
// let envmap =generator.fromEquirectangular(texture)
texture.mapping = THREE.EquirectangularReflectionMapping;
// scene.background = texture;
scene.environment = texture;
});
loader.load(path, function (gltf) {
model.current = gltf.scene;
model.current.position.x=0
model.current.position.y=-3
scene.add(model.current);
setLoading(false);
}, undefined, function (error) {
console.error(error)
});
}

How to keep my mapview.markers from rescaling when calling setGlobalState?

So i am using react native or more specifically expo mapview to render a mapview component with some custom markers. Theese markers are then connected to a scrollview in a way such that as when the scrollview is scrolled different markers corresponding to what is shown is then rescaled, aka gets 1.5x bigger this is also periodic so when im in between two they are both like 1.25x bigger code for this map with markers show below.
<MapView
ref={refMap}
style={styles.map}
region={state.region}
scrollEnabled={true}
zoomEnabled={true}
zoomTapEnabled={false}
rotateEnabled={true}
showsPointsOfInterest={false}
moveOnMarkerPress={false} // android only
minZoomLevel={13}
maxZoomLevel={16}
onMapReady={(resetMap)}
//onMarkerPress={onMarkerPress}
>
{/* creates markers on map with unique index; coordinates, img, title from mapData*/}
{state.markers.map((marker, index) => {
const scaleStyle = { // creates a scaleStyle by transforming/scaling the marker in func interpolations
transform: [
{
scale: interpolations[index].scale,
},
],
};
return (
<MapView.Marker
key={index}
coordinate={marker.coordinate}
onPress={onMarkerPress}
>
<Animated.View style={styles.markerWrap}>
<Animated.Image
source={marker.image}
style={[styles.marker, scaleStyle]} // adding the scaleStyle to the marker img (which is the marker)
//resizeMode= 'cover'
>
</Animated.Image>
</Animated.View>
</MapView.Marker>
);
})}
</MapView>
//function codes seen here
let markerAnimation = new Animated.Value(0);
//console.log(markerAnimation)
// Called when creating the markers (markers and cards has matching indexes)
// Setting inputRange (card index/position) for the animation, mapping it to an outputRange (how the marker should be scaled)
const interpolations = state.cards.map((card, index) => {
const inputRange = [
(index - 1) * Dimensions.get('window').width,
index * Dimensions.get('window').width,
((index + 1) * Dimensions.get('window').width),
];
const scale = markerAnimation.interpolate({
inputRange, //card index/positions [before, present, after]
outputRange: [1, 1.5, 1], //scales the marker [before, present, after]
extrapolate: "clamp"
});
return { scale };
});
// Called when a marker is pressed
const onMarkerPress = (mapEventData) => {
const markerIndex = mapEventData._targetInst.return.key; // returns the marker's index
let x = (markerIndex * Dimensions.get('window').width); // setting card x-position
refCards.current.scrollTo({x: x, y: 0, animated: true}); // scroll to x-position with animation
};
And whilst this whole interaction has been working exactly like i imagined i have this button that takes me to another page in the app which on arrival needs to fetch some data trough an api call. and this data depends on the index of the scrollview. In this other screen where the scrollview is also present i just use the onMomentumScrollEnd to set a global state value to the corresponding value(code for this global state shown below) this works but i would like to do exacty the same thing in the screen containing the map. but whenever a global state change is called the scaling "resets" so that the first one is the one "highlighted" even tho the scrollview is somewhere completly different.
//this is a index.js containing the store
import { createGlobalState } from 'react-hooks-global-state';
const region = {
latitude: 59.85843,
longitude: 17.63356,
latitudeDelta: 0.01,
longitudeDelta: 0.0095,
}
const { setGlobalState, useGlobalState } = createGlobalState({
currentIndex: 0,
initialMapState: {
markers: [],
region: region,
cards: [],
}
}
)
//and in a screen this can be used to change/read
import {setGlobalState, useGlobalState} from '../state' //imports it
const [currentIndex] = useGlobalState("currentIndex") //reads value
//and this is how i use the onMomentumScrollend
const onMomentumScrollEnd = ({ nativeEvent }) => {
const index = Math.round(nativeEvent.contentOffset.x / Dimensions.get('window').width);
if (index !== currentIndex) {
setGlobalState('currentIndex',index)
}
};
Any help is greatly appreciated as well im simply lost, i have tried storing the index in some sort of ref which worked fine and then try to update it on some sort of callback on the useFocusEffect but this got buggy real fast and ended up not really working. I have also tried to google to find some sort of way to keep the markers from not rerendering on state change, something which seems impossible? altough i think it would solve my problem as the markers dont depend on the state at all.
*edit
I also feel like i forgot to add that i am extremly happy to switch to any other sort of global state manager if it contains some sort of way to keep this fromh happening
**edit
So after revising this problem i found a soloution which i am not happy with, but is working.
useEffect(() => {
//scrolls back without animation when currentIndex is changed to handle react autorerender
//And scrolls if value is changed in eventscreen
refCards.current.scrollTo({
x: currentIndex * Dimensions.get("window").width,
y: 0,
animated: false,
});
}, [currentIndex]);

Responsive flat panel in react-360

I would like to make responsive flat panel - on a mobile device should be in full screen like there
https://tour.croptrust.org/
Is there a possibility to achieve that in react-360.
Here is my example https://gstuczynski.com/tour/plaszow/ - try to hit loupe -> on the desktop looks good but how to make it responsive?
Here is my code
https://github.com/gstuczynski/plaszow-cc-virtual-tour
If you're looking to set your surface size based on whether user is on mobile or desktop, I'd approach this by building a NativeModule to check if the device is mobile on componentDidMount() and using the built in resize() function to adjust your surface sizes.
First add a module in client.js
function init(bundle, parent, options = {}) {
const r360 = new ReactInstance(bundle, parent, {
nativeModules: [
//new module added here
ctx => new SurfaceModule(ctx),
],
fullScreen: true,
...options,
});
}
In your case you already have vars for the surfaces - but remove const to expose them to your NativeModule:
infoPanel = new Surface(1440, 850, Surface.SurfaceShape.Flat);
mapPanel = new Surface(850, 800, Surface.SurfaceShape.Flat);
cylinderSurface = new Surface(4096, 720, Surface.SurfaceShape.Cylinder);
Next, create your class SurfaceModule and include a function to check for mobile and include a ref for the surface namecheckMobile(surfaceName)
import {Module} from 'react-360-web';
export default class SurfaceModule extends Module {
constructor(ctx) {
super('SurfaceModule');
this._ctx = ctx;
}
checkMobile(surfaceName, id) {
if (navigator.userAgent.match('add your match criteria here'))
{
let swidth = screen.width
let sheight = screen.height
surfaceName.resize(swidth, sheight);
this._ctx.invokeCallback(
id,
[swidth, sheight]
);
}
}
}
Finally in index.js run the function to check and adjust surface sizes.
With this setup you'll need to call the function for each Surface you want to check or you could rework to send multiple refs and perform an if/switch
You could also move the call out of componentDidMount() into another func
import {
NativeModules
} from 'react-360';
const SurfaceModule = NativeModules.SurfaceModule;
componentDidMount() {
SurfaceModule.checkMobile((swidth, sheight) => {
console.log(swidth);
console.log(sheight);
});
}
EDIT: Updated checkMobile() func & index.js to include callback

How to include GreenSock plugin for ScrollMagic in a ReactJS environment?

How do I include the Greensock plugin for ScrollMagic in my ReactJS project?
The authors of ScrollMagic made plugins to incorporate libraries like GreenSock and Velocity. These work great when you simply include them in your head of your html doc like so
<script type="text/javascript" src="js/lib/greensock/TweenMax.min.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/ScrollMagic.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/plugins/animation.gsap.js"></script>
But when you're in ReactJS, you don't import javascript resources like this. You actually have to import them through processes like the npm command, then declare them in your react project like
import ScrollMagic from "scrollmagic"
Although I was able to import ScrollMagic into react files and start using scrollmagic, I haven't been able to import the greensock plugins. There's no documentation on how to do this. I tried to hack things apart by taking segments of code in animation.gsap.js and pasting it into the node_modules/scrollmagic/scrollmagic.js file (which isn't a good idea to be editing these files), but it either breaks the webpack compiler or it breaks my project code.
How do I use the greensock plugin for scrollmagic in a react environment?
I was able to build a wrapper. At a high level, what I did was study the code of plugins/animation.gsap.js, pulled out those Scene extended properties that I needed, changed the name space, and have it augment the behaviour of ScrollMagic in a separate react class prior to exporting it.
Specifically, what I did was create a new file called ./ScrollMagic.js and pasted the following contents:
import ScrollMagic from 'scrollmagic';
import {TweenLite as Tween,TimelineMax as Timeline} from 'gsap';
ScrollMagic.Scene.addOption("tweenChanges", // name
false, // default
function (val) { // validation callback
return !!val;
});
ScrollMagic.Scene.extend(function () {
var Scene = this,
_tween;
var log = function () {
if (Scene._log) { // not available, when main source minified
Array.prototype.splice.call(arguments, 1, 0, "(animation.gsap)", "->");
Scene._log.apply(this, arguments);
}
};
// set listeners
Scene.on("progress.plugin_gsap", function () {
updateTweenProgress();
});
Scene.on("destroy.plugin_gsap", function (e) {
Scene.removeTween(e.reset);
});
/**
* Update the tween progress to current position.
* #private
*/
var updateTweenProgress = function () {
if (_tween) {
var
progress = Scene.progress(),
state = Scene.state();
if (_tween.repeat && _tween.repeat() === -1) {
// infinite loop, so not in relation to progress
if (state === 'DURING' && _tween.paused()) {
_tween.play();
} else if (state !== 'DURING' && !_tween.paused()) {
_tween.pause();
}
} else if (progress != _tween.progress()) { // do we even need to update the progress?
// no infinite loop - so should we just play or go to a specific point in time?
if (Scene.duration() === 0) {
// play the animation
if (progress > 0) { // play from 0 to 1
_tween.play();
} else { // play from 1 to 0
_tween.reverse();
}
} else {
// go to a specific point in time
if (Scene.tweenChanges() && _tween.tweenTo) {
// go smooth
_tween.tweenTo(progress * _tween.duration());
} else {
// just hard set it
_tween.progress(progress).pause();
}
}
}
}
};
/**
* Add a tween to the scene.
* If you want to add multiple tweens, add them into a GSAP Timeline object and supply it instead (see example below).
*
* If the scene has a duration, the tween's duration will be projected to the scroll distance of the scene, meaning its progress will be synced to scrollbar movement.
* For a scene with a duration of `0`, the tween will be triggered when scrolling forward past the scene's trigger position and reversed, when scrolling back.
* To gain better understanding, check out the [Simple Tweening example](../examples/basic/simple_tweening.html).
*
* Instead of supplying a tween this method can also be used as a shorthand for `TweenMax.to()` (see example below).
* #memberof! animation.GSAP#
*
* #example
* // add a single tween directly
* scene.setTween(TweenMax.to("obj"), 1, {x: 100});
*
* // add a single tween via variable
* var tween = TweenMax.to("obj"), 1, {x: 100};
* scene.setTween(tween);
*
* // add multiple tweens, wrapped in a timeline.
* var timeline = new TimelineMax();
* var tween1 = TweenMax.from("obj1", 1, {x: 100});
* var tween2 = TweenMax.to("obj2", 1, {y: 100});
* timeline
* .add(tween1)
* .add(tween2);
* scene.addTween(timeline);
*
* // short hand to add a TweenMax.to() tween
* scene.setTween("obj3", 0.5, {y: 100});
*
* // short hand to add a TweenMax.to() tween for 1 second
* // this is useful, when the scene has a duration and the tween duration isn't important anyway
* scene.setTween("obj3", {y: 100});
*
* #param {(object|string)} TweenObject - A TweenMax, TweenLite, TimelineMax or TimelineLite object that should be animated in the scene. Can also be a Dom Element or Selector, when using direct tween definition (see examples).
* #param {(number|object)} duration - A duration for the tween, or tween parameters. If an object containing parameters are supplied, a default duration of 1 will be used.
* #param {object} params - The parameters for the tween
* #returns {Scene} Parent object for chaining.
*/
Scene.setTween = function (TweenObject, duration, params) {
var newTween;
if (arguments.length > 1) {
if (arguments.length < 3) {
params = duration;
duration = 1;
}
TweenObject = Tween.to(TweenObject, duration, params);
}
try {
// wrap Tween into a Timeline Object if available to include delay and repeats in the duration and standardize methods.
if (Timeline) {
newTween = new Timeline({
smoothChildTiming: true
}).add(TweenObject);
} else {
newTween = TweenObject;
}
newTween.pause();
} catch (e) {
log(1, "ERROR calling method 'setTween()': Supplied argument is not a valid TweenObject");
return Scene;
}
if (_tween) { // kill old tween?
Scene.removeTween();
}
_tween = newTween;
// some properties need to be transferred it to the wrapper, otherwise they would get lost.
if (TweenObject.repeat && TweenObject.repeat() === -1) { // TweenMax or TimelineMax Object?
_tween.repeat(-1);
_tween.yoyo(TweenObject.yoyo());
}
// Some tween validations and debugging helpers
if (Scene.tweenChanges() && !_tween.tweenTo) {
log(2, "WARNING: tweenChanges will only work if the TimelineMax object is available for ScrollMagic.");
}
// check if there are position tweens defined for the trigger and warn about it :)
if (_tween && Scene.controller() && Scene.triggerElement() && Scene.loglevel() >= 2) { // controller is needed to know scroll direction.
var
triggerTweens = Tween.getTweensOf(Scene.triggerElement()),
vertical = Scene.controller().info("vertical");
triggerTweens.forEach(function (value, index) {
var
tweenvars = value.vars.css || value.vars,
condition = vertical ? (tweenvars.top !== undefined || tweenvars.bottom !== undefined) : (tweenvars.left !== undefined || tweenvars.right !== undefined);
if (condition) {
log(2, "WARNING: Tweening the position of the trigger element affects the scene timing and should be avoided!");
return false;
}
});
}
// warn about tween overwrites, when an element is tweened multiple times
if (parseFloat(TweenLite.version) >= 1.14) { // onOverwrite only present since GSAP v1.14.0
var
list = _tween.getChildren ? _tween.getChildren(true, true, false) : [_tween],
// get all nested tween objects
newCallback = function () {
log(2, "WARNING: tween was overwritten by another. To learn how to avoid this issue see here: https://github.com/janpaepke/ScrollMagic/wiki/WARNING:-tween-was-overwritten-by-another");
};
for (var i = 0, thisTween, oldCallback; i < list.length; i++) { /*jshint loopfunc: true */
thisTween = list[i];
if (oldCallback !== newCallback) { // if tweens is added more than once
oldCallback = thisTween.vars.onOverwrite;
thisTween.vars.onOverwrite = function () {
if (oldCallback) {
oldCallback.apply(this, arguments);
}
newCallback.apply(this, arguments);
};
}
}
}
log(3, "added tween");
updateTweenProgress();
return Scene;
};
/**
* Remove the tween from the scene.
* This will terminate the control of the Scene over the tween.
*
* Using the reset option you can decide if the tween should remain in the current state or be rewound to set the target elements back to the state they were in before the tween was added to the scene.
* #memberof! animation.GSAP#
*
* #example
* // remove the tween from the scene without resetting it
* scene.removeTween();
*
* // remove the tween from the scene and reset it to initial position
* scene.removeTween(true);
*
* #param {boolean} [reset=false] - If `true` the tween will be reset to its initial values.
* #returns {Scene} Parent object for chaining.
*/
Scene.removeTween = function (reset) {
if (_tween) {
if (reset) {
_tween.progress(0).pause();
}
_tween.kill();
_tween = undefined;
log(3, "removed tween (reset: " + (reset ? "true" : "false") + ")");
}
return Scene;
};
});
export default ScrollMagic;
You will notice that this looks almost exactly like plugins/animation.gsap.js, with the exceptions being:
I added my own import lines at the top, so make sure you have npm installed scrollmagic and gsap ahead of time
I export the ScrollMagic object towards the end
plugins/animation.gsap.js had some other code that turn the plugin into a factory which I excluded because it doesn't apply here
Now I can use .setTween() in my react project. Example usage:
import React, {Component} from "react";
import ScrollMagic from "./ScrollMagic"; // my own wrapper for scrollmagic that includes greensock
export default class Home extends Component {
componentDidMount()
{
var controller = new ScrollMagic.Controller();
var item = "#whateverstuffselector";
var scene = new ScrollMagic.Scene({triggerElement:item})
.setTween(item, 0.5, {backgroundColor: "red", scale: 3})
.addTo(controller);
}
render()
{
return (<div id="whateverstuffselector">stuff</div>);
}
}
After escalating this issue for 1 month I guess I found great solution.
So this issue shows that in React environment we can not get animation.gsap file.
This fix does not require any webpack changes except animation.gsap file itself.
Find these files in "node_module" directory tree (may have different location on you PC) and import it in this way to your working JS file (App.js for example).
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/animation.gsap";
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/debug.addIndicators";
Go to animation.gsap and add these two lines of code at the beginning of file.
import { TimelineMax, TweenMax, TweenLite} from "gsap/all";
import ScrollMagic from "scrollmagic";
Go to debug.addIndicators and add this line of code at the beginning of file (in case if you need indicator debugger, but I strongly suggest not to skip this step).
import ScrollMagic from "scrollmagic";
In animation.gsap find first function and delete all "root" variables and change them to variables that I provided below. ( you should find 8 of them).
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic), root.TweenMax || root.TweenLite, root.TimelineMax || root.TimelineLite);
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic), TweenMax || TweenLite, TimelineMax || TimelineLite);
}
}
In debug.addIndicators also delete all "root" variables ( you should find 4 of them ) and change them to variables that I provided below.
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic));
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic));
}
}
I hope this solution will work for you.
In any case you can reach me for help.

How to communicate an action to a React component

While my scenario is pretty specific, I think it speaks to a bigger question in Flux. Components should be simple renderings of data from a store, but what if your component renders a third-party component which is stateful? How does one interact with this third-party component while still obeying the rules of Flux?
So, I have a React app that contains a video player (using clappr). A good example is seeking. When I click a location on the progress bar, I want to seek the video player. Here is what I have right now (using RefluxJS). I've tried to strip down my code to the most relevant parts.
var PlayerActions = Reflux.createActions([
'seek'
]);
var PlayerStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.data.seekTo = seekTo;
this.trigger(this.data);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(PlayerStore, 'onStoreChange')],
onStoreChange(data) {
if (data.seekTo !== this.state.seekTo) {
window.player.seek(data.seekTo);
}
// apply state
this.setState(data);
}
componentDidMount() {
// build a player
window.player = new Clappr.Player({
source: this.props.sourcePath,
chromeless: true,
useDvrControls: true,
parentId: '#player',
width: this.props.width
});
},
componentWillUnmount() {
window.player.destroy();
window.player = null;
},
shouldComponentUpdate() {
// if React realized we were manipulating DOM, it'd certainly freak out
return false;
},
render() {
return <div id='player'/>;
}
});
The bug I have with this code is when you try to seek to the same place twice. Imagine the video player continuously playing. Click on the progress bar to seek. Don't move the mouse, and wait a few seconds. Click on the progress bar again on the same place as before. The value of data.seekTo did not change, so window.player.seek is not called the second time.
I've considered a few possibilities to solve this, but I'm not sure which is more correct. Input requested...
1: Reset seekTo after it is used
Simply resetting seekTo seems like the simplest solution, though it's certainly no more elegant. Ultimately, this feels more like a band-aid.
This would be as simple as ...
window.player.on('player_seek', PlayerActions.resetSeek);
2: Create a separate store that acts more like a pass-through
Basically, I would listen to a SeekStore, but in reality, this would act as a pass-through, making it more like an action that a store. This solution feels like a hack of Flux, but I think it would work.
var PlayerActions = Reflux.createActions([
'seek'
]);
var SeekStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.trigger(seekTo);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(SeekStore, 'onStoreChange')],
onStoreChange(seekTo) {
window.player.seek(seekTo);
}
});
3: Interact with window.player within my actions
When I think about it, this feels correct, since calling window.player.seek is in fact an action. The only weird bit is that I don't feel right interacting with window inside the actions. Maybe that's just an irrational thought, though.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
if (window.player) {
try {
window.player.seek(seekTo);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
} else {
PlayerActions.seek.failed(new Error('player not initialized'));
}
});
BTW, there's a whole other elephant in the room that I didn't touch on. In all of my examples, the player is stored as window.player. Clappr did this automatically in older versions, but though it has since been fixed to work with Browserify, we continue to store it on the window (tech debt). Obviously, my third solution is leveraging that fact, which it technically a bad thing to be doing. Anyway, before anyone points that out, understood and noted.
4: Seek via dispatchEvent
I also understand that dispatching a custom event would get the job done, but this feels way wrong considering I have Flux in place. This feels like I'm going outside of my Flux architecture to get the job done. I should be able to do it and stay inside the Flux playground.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
try {
let event = new window.CustomEvent('seekEvent', {detail: seekTo});
window.dispatchEvent(event);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
});
var Player = React.createClass({
componentDidMount() {
window.addEventListener('seekEvent', this.onSeek);
},
componentWillUnmount() {
window.removeEventListener('seekEvent', this.onSeek);
},
onSeek(e) {
window.player.seek(e.detail);
}
});
5: keep the playing position in state (as noted by Dan Kaufman)
Could be done something like this:
handlePlay () {
this._interval = setInterval(() => this.setState({curPos: this.state.curPos + 1}), 1000)
this.setState({playing: true}) // might not be needed
}
handlePauserOrStop () {
clearInterval(this._interval)
this.setState({playing: false})
}
componentWillUnmount () {
clearInteral(this._interval)
}
onStoreChange (data) {
const diff = Math.abs(data.seekTo - this.state.curPos)
if (diff > 2) { // adjust 2 to your unit of time
window.player.seek(data.seekTo);
}
}

Resources