HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<h1 class="title">Create a flake</h1>
<button class="button button--next" id="js-next-button">
<span>Next</span>
</button>
<button class="button button--start-again" id="js-start-again-button">
<span>Create another</span>
</button>
<div class="knife-cursor" id="cursor"></div>
<div class="demo-animation__touch-marker" id="touch-marker"></div>
* {
user-select: none;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
background: #bd523b url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/background.jpg') no-repeat center;
background-size: cover;
font-family: "Karla", Arial, sans-serif;
font-weight: bold;
}
#paper-texture {
display: none;
}
.cutting-mode .segment-canvas {
opacity: 1;
}
.cutting-mode .snowflake-3d {
opacity: 0;
}
.snowflake-3d {
transition: 500ms opacity ease-out;
}
.segment-canvas {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
width: 26%;
height: 62%;
transform: translateX(-50%) translateY(-50%);
transition: 500ms opacity ease-out;
}
.ants-canvas {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 3;
cursor: none;
}
.button {
position: absolute;
bottom: 40px;
right: 50px;
display: none;
padding: 10px 50px 10px 15px;
overflow: hidden;
font-size: 18px;
font-weight: bold;
text-transform: uppercase;
color: #fff;
background: none;
border: 2px solid #fff;
border-radius: 30px;
z-index: 4;
cursor: pointer;
}
@media screen and (max-width: 600px) {
.button--start-again {
right: auto;
left: 50%;
width: 240px;
transform: translateX(-50%);
}
}
.button--active {
display: block;
}
.cutting-mode .button--next {
display: block;
}
.button span {
position: relative;
}
.button:after {
position: absolute;
top: 50%;
right: 10px;
display: block;
width: 28px;
height: 28px;
margin-top: -14px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/button-icon.png') no-repeat center;
background-size: contain;
content: '';
}
.button:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: #bd523b;
transform: scaleX(0);
transform-origin: 0;
content: '';
transition: 250ms transform ease-out;
}
.button:hover:before {
transform: scaleX(1);
}
.knife-cursor {
position: absolute;
top: 0;
left: 0;
z-index: 5;
display: none;
width: 45px;
height: 45px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/scalpel.png') no-repeat center;
mix-blend-mode: difference;
cursor: none;
pointer-events: none;
}
.cutting-mode .knife-cursor {
display: block;
}
.demo-animation__touch-marker {
position: absolute;
top: 0;
left: 0;
margin-top: -25px;
margin-left: -25px;
z-index: 6;
width: 50px;
height: 50px;
transform-origin: 50%;
opacity: 0;
background: #3eacc3;
border-radius: 50%;
}
.title {
position: absolute;
top: 10%;
margin: 0;
width: 100%;
text-align: center;
font-weight: bold;
text-transform: uppercase;
color: #fff;
}
.title:before {
position: absolute;
top: -35px;
left: 50%;
display: block;
width: 60px;
height: 30px;
margin-left: -30px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/flourish.png') no-repeat center;
background-size: contain;
content: '';
}
/**
* Baked at toaster.co by Will Donohoe
*
*/
var SnowflakeApp = function() {
/**
* @type {THREE.PerspectiveCamera}
* @private
*/
this.camera_ = null;
/**
* @type {THREE.Scene}
* @private
*/
this.scene_ = null;
/**
* @type {THREE.WebGLRenderer}
* @private
*/
this.renderer_ = null;
/**
* @type {THREE.OrbitControls}
* @private
*/
this.controls_ = null;
/**
* The snowflake controller, manages the 3D snowflake animation and initiation.
* @type {Snowflake}
* @private
*/
this.snowflake_ = null;
/**
* Manages the cutting stage of the application, creates the 2D canvas and
* drawing tools.
* @type {CuttingStage}
* @private
*/
this.cuttingArea_ = null;
/**
* Stores the start again button that appears at the end.
* @type {Element}
* @private
*/
this.startAgainButton_ = null;
/**
* Cache the initial position of the camera. It will be used to translate back
* to the original position restarting the app.
* @type {THREE.Vector3}
* @private
*/
this.cameraInitialPosition_ = new THREE.Vector3();
};
/**
* Setup standard THREE js scene, renderer, cameras, lights and controls.
* App bootup.
*/
SnowflakeApp.prototype.init = function() {
this.camera_ = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 15000);
this.camera_.position.z = 10;
this.camera_.position.y = 5;
this.cameraInitialPosition_ = this.camera_.position.clone();
this.camera_.lookAt(new THREE.Vector3(0, 0, 0));
this.scene_ = new THREE.Scene();
this.renderer_ = new THREE.WebGLRenderer({ alpha: true });
this.renderer_.setPixelRatio(window.devicePixelRatio);
this.renderer_.setSize(window.innerWidth, window.innerHeight);
this.renderer_.shadowMap.enabled = true;
this.controls_ = new THREE.OrbitControls(this.camera_, this.renderer_.domElement);
// this.controls_.enableDamping = true;
this.controls_.dampingFactor = 0.25;
this.controls_.enabled = false;
this.controls_.enableZoom = false;
this.controls_.autoRotateSpeed = 0.5;
var spotLight = new THREE.SpotLight( 0xffffff );
spotLight.castShadow = true;
spotLight.position.set(-10, 3, -2);
spotLight.castShadow = true;
spotLight.angle = 0.3;
spotLight.penumbra = 0.2;
spotLight.decay = 2;
spotLight.distance = 30;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 0;
spotLight.shadow.camera.far = 10;
spotLight.shadow.camera.fov = 2;
this.scene_.add( spotLight );
var ambient = new THREE.AmbientLight(0xffffff, 1);
this.scene_.add(ambient);
this.scene_.add(spotLight.target);
document.body.appendChild(this.renderer_.domElement);
this.renderer_.domElement.classList.add('snowflake-3d');
window.addEventListener('resize', this.onResize_.bind(this), false);
this.startAgainButton_ = document.getElementById('js-start-again-button');
this.startAgainButton_.addEventListener('click', this.startAgain_.bind(this), false);
this.addSnowflake_();
this.update_();
};
/**
* Fired when the start again button is clicked.
* Reset the app back to the snowflake intro animation.
* @private
*/
SnowflakeApp.prototype.startAgain_ = function() {
this.startAgainButton_.classList.remove('button--active');
this.snowflake_.reset();
// Disable the controls.
this.controls_.enabled = false;
this.controls_.autoRotate = false;
// Animate the camera back to the initial position.
var tl = new TimelineMax();
tl.to(this.camera_.position, .5, {
x: this.cameraInitialPosition_.x,
y: this.cameraInitialPosition_.y,
z: this.cameraInitialPosition_.z
});
};
/**
* Initiate the snowflake.
* @private
*/
SnowflakeApp.prototype.addSnowflake_ = function() {
this.snowflake_ = new Snowflake();
this.snowflake_.init();
this.snowflake_.addEventListener(Snowflake.Events.FOLDING_ANIMATION_COMPLETE,
this.initDrawMode_.bind(this));
this.scene_.add(this.snowflake_.mesh);
};
/**
* Initiate the cutting stage.
* @private
*/
SnowflakeApp.prototype.initDrawMode_ = function() {
this.cuttingArea_ = new CuttingStage();
this.cuttingArea_.init();
this.cuttingArea_.addEventListener(CuttingStage.Events.COMPLETED,
this.cuttingComplete_.bind(this), false);
};
/**
* Fired when CuttingStage.Events.COMPLETED is sent. The segment canvas is sent
* with the event. Create a new snowflakeTexture to create the whole texture.
*
* @param e
* @private
*/
SnowflakeApp.prototype.cuttingComplete_ = function(e) {
var snowflakeTexture = new SnowflakeTexture();
// Create the snowflake texture from segment.
var snowflakeImage = snowflakeTexture.createTexture(e.segment);
// Send the texture to the snowflake.
this.snowflake_.addSnowflakeTexture(snowflakeImage);
// Destroy the cuttingArea, a new instance will be created if the user
// restarts the app.
this.cuttingArea_.destroy();
this.cuttingArea_ = null;
// After 250ms, run the unfolding animation, and re-enable the orbit controls.
setTimeout(function() {
this.snowflake_.unfold();
this.startAgainButton_.classList.add('button--active');
this.controls_.enabled = true;
this.controls_.autoRotate = true;
}.bind(this), 250);
};
/**
* The update loop.
* @private
*/
SnowflakeApp.prototype.update_ = function() {
this.controls_.update();
this.render_();
requestAnimationFrame(this.update_.bind(this));
};
/**
* When the window is resized, update THREE with new window size, along with
* the cutting area if it exists.
* @private
*/
SnowflakeApp.prototype.onResize_ = function() {
var w = window.innerWidth;
var h = window.innerHeight;
this.camera_.aspect = w / h;
this.camera_.updateProjectionMatrix();
this.renderer_.setSize(w, h);
if (this.cuttingArea_) {
this.cuttingArea_.onResize();
}
};
/**
* Render the scene.
* @private
*/
SnowflakeApp.prototype.render_ = function() {
this.renderer_.render(this.scene_, this.camera_);
};
// DEBOUNCE METHOD
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
var debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
/**
* Is the user on iOS? I'm not a fan of user agent sniffing, but sometimes it's
* necessary.
* @return {Boolean}
*/
var isIOS = function() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
};
// SNOWFLAKE
/**
* Manages the 3D snowflake, along with the folding and unfolding animations.
*
* @constructor
*/
var Snowflake = function() {
/**
* The snowflake mesh.
* @type {THREE.Mesh}
*/
this.mesh = null;
/**
* Store the current index in the morph targets array.
* @type {number}
* @private
*/
this.animationStage_ = 0;
/**
* The TimelineMax instance for the main folding and unfolding animation.
* @type {TimelineMax}
* @private
*/
this.animationTimeline_ = null;
/**
* Store the initial paper texture, to re-apply if the user resets.
* @type {THREE.Texture}
* @private
*/
this.initialTexture_ = null;
};
// Extend the THREE.EventDispatcher prototype, to allow events to be fired from
// this class.
Object.assign(Snowflake.prototype, THREE.EventDispatcher.prototype);
/**
* Initiate the snowflake, load the geometry, create the material, and load
* the paper texture.
*/
Snowflake.prototype.init = function() {
var textureLoader = new THREE.TextureLoader();
textureLoader.crossOrigin = 'anonymous';
var material = new THREE.MeshLambertMaterial({
side: THREE.DoubleSide,
morphTargets: true,
morphNormals: true,
transparent: true,
alphaTest: 0.5,
color: 0xffffff
});
if (!isIOS()) {
// Load the paper texture, once loaded, apply it to the material.
textureLoader.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/paper-tex2.png', function(texture) {
material.map = texture;
material.needsUpdate = true;
// Store for later use.
this.initialTexture_ = texture;
}.bind(this));
}
// Instantiate a loader
var loader = new THREE.JSONLoader();
var model = loader.parse(Snowflake.data);
model.geometry.computeMorphNormals();
// Create the mesh from the loaded geometry and material.
this.mesh = new THREE.Mesh(model.geometry, material);
this.mesh.castShadow = true;
this.mesh.receiveShadow = true;
this.mesh.position.set(0, -7, -1.2);
this.mesh.rotation.set(
THREE.Math.degToRad(-5.7),
THREE.Math.degToRad(-45.8),
0
);
// Wait for 2 seconds and start the folding animation.
setTimeout(function() {
this.playFoldingUpAnimation_();
}.bind(this), 2000);
};
/**
* Create the unfolding animation timeline.
*/
Snowflake.prototype.unfold = function() {
// Get the morph target animation.
this.animationTimeline_ = this.createTimeline_(-1);
// Get the amount of time it takes to complete the morph targets animation.
var time = this.animationTimeline_.totalDuration();
// Set position and rotation animations based off the computed time.
this.animationTimeline_.to(this.mesh.position, time, {
x: -0.7,
y: 1.2,
z: 0,
ease: Power2.easeInOut
}, 0);
this.animationTimeline_.to(this.mesh.rotation, time, {
x: THREE.Math.degToRad(67),
y: THREE.Math.degToRad(166),
z: THREE.Math.degToRad(-358),
ease: Power2.easeInOut
}, 0.5);
};
/**
* Create and play the folding animation.
* @private
*/
Snowflake.prototype.playFoldingUpAnimation_ = function() {
// Create the morph targets animation.
this.animationTimeline_ = this.createTimeline_(1, this.onFoldingAnimationComplete_.bind(this));
// Get the amount of time it takes to animate the morph targets.
var time = this.animationTimeline_.totalDuration();
// Add position and rotation of the mesh to the timeline, based off the
// computed time.
this.animationTimeline_.to(this.mesh.position, 0.5, {
x: 0,
y: -1,
z: 0.8,
ease: Power2.easeInOut
}, 0);
this.animationTimeline_.to(this.mesh.rotation, time - 0.5, {
x: THREE.Math.degToRad(-90),
y: THREE.Math.degToRad(-40),
z: THREE.Math.degToRad(22.9),
ease: Power2.easeInOut
}, 0.3);
this.animationTimeline_.to(this.mesh.position, time - 0.5, {
x: 0.04,
y: 2.69,
z: 5.44,
ease: Power2.easeInOut
}, 0.5);
};
/**
* This method creates a timeline, which goes through the morph targets array
* and interpolates each target between 0 and 1 to create a seamless animation.
*
* @param {number} direction - Use 1 for forwards (folding up), -1 for backwards (unfolding).
* @param {Function} callback - A function to run when the morph targets animation has completed.
* @return {TimelineMax}
* @private
*/
Snowflake.prototype.createTimeline_ = function(direction, callback) {
var stages = 8;
// Set the animationStage depending on direction.
this.animationStage_ = direction === 1 ? 0 : stages;
// Get the next animation stage.
this.getNextAnimationStage_(this.animationStage_, direction);
// Create the timeline, which should run in sequence.
var timeline = new TimelineMax({
align: 'sequence',
onComplete: callback
});
// The amount of time each animation takes.
var stageDuration = 0.5;
// The amount of time the next animation should start before the last finishes.
var stageOverlap = 0.3;
var i = 0;
var morphTo;
// Depending on the direction, queue the morphTargetInfluence animations in
// stages.
if (direction === 1) {
for (i = 0; i < stages; i++) {
morphTo = this.getNextAnimationStage_(i, direction);
timeline.to(this.mesh.morphTargetInfluences, stageDuration, morphTo, '-=' + stageOverlap);
}
} else {
for (i = stages; i > 0; i--) {
morphTo = this.getNextAnimationStage_(i, direction);
timeline.to(this.mesh.morphTargetInfluences, stageDuration, morphTo, '-=' + stageOverlap);
}
}
return timeline;
};
/**
* Create a new array with the next set of morph targets to interpolate to.
* @param {number} stage - The animation stage.
* @param {number} direction - The direction (-1 for backwards, 1 for forwards).
* @return {Array}
* @private
*/
Snowflake.prototype.getNextAnimationStage_ = function(stage, direction) {
// Duplicate the morphTargetInfluences array.
var morphTo = this.mesh.morphTargetInfluences.slice(0);
// Set all morph targets to 0.
for (var i = 0; i < morphTo.length; i++) {
morphTo[i] = 0;
}
// Find the next stage and set to 1.
var nextStage = stage + direction;
morphTo[nextStage] = 1;
return morphTo;
};
/**
* Fired when the folding animation is complete. Dispatch an event to notify
* the parent.
* @private
*/
Snowflake.prototype.onFoldingAnimationComplete_ = function() {
this.dispatchEvent({
type: Snowflake.Events.FOLDING_ANIMATION_COMPLETE
});
};
/**
* Create a new texture with the generated canvas and apply it to the material.
* @param {HTMLCanvasElement} canvas
*/
Snowflake.prototype.addSnowflakeTexture = function(canvas) {
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
this.mesh.material.map = texture;
this.mesh.material.needsUpdate = true;
};
/**
* Reset the snowflake. Create a new Timeline to animate teh current snowflake
* off-screen, reset the texture and play the folding animation again.
*/
Snowflake.prototype.reset = function() {
var timeline = new TimelineMax({
align: 'sequence'
});
timeline.to(this.mesh.position, 1, {
y: -10
});
timeline.set(this.mesh.position, {
x: 0,
y: 5,
z: 0
});
timeline.set(this.mesh.rotation, {
x: 0,
y: 0,
z: 0
});
timeline.addCallback(this.resetTexture_.bind(this));
timeline.addCallback(this.playFoldingUpAnimation_.bind(this));
};
/**
* Set the texture to the initial paper texture that was loaded at the beginning.
* @private
*/
Snowflake.prototype.resetTexture_ = function() {
this.mesh.material.map = this.initialTexture_;
this.mesh.material.needsUpdate = true;
};
/**
* The snowflake geometry, uvs and animations were generated created in Blender.
* The morph targets were modified slightly to milestones instead of a
* frame-by-frame animation. This reduces the amount of data needed considerably.
* @type {Object}
*/
Snowflake.data = {
"metadata":{
"type":"Geometry",
"normals":9,
"uvs":1,
"generator":"io_three",
"morphTargets":250,
"version":3,
"faces":12,
"vertices":16
},
"name":"PlaneGeometry.502",
"uvs":[[0.170373,1.09826,0.510954,0.500103,0.517938,1.00289,0.073923,0.746748,0.009312,0.511277,-0.087204,0.159522,0.761375,0.936097,1.10911,0.840683,0.259988,0.064257,1.01241,0.488241,0.948011,0.253553,0.851534,-0.098055,0.503382,-0.002527]],
"normals":[0,-0,-1,0,-0,-1,0,-0,-1,-0,-0,-1,-0,-0,-1,-0,-0,-1,0,-0,-1,-0,-0,-1,0,-0,-1],
"morphTargets":[{
"name":"animation_000000",
"vertices":[-1.04052,0,1.92489,-3.84915,0.015564,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,-2.79745,0.009736,0.158219,-2.09294,0.005832,0.866636,-2.08092,0.005765,-2.67734,-2.80927,0.009802,-1.94495,-1.04052,0,1.92489,1.78368,-0,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,0.725862,-0,-1.95712,0.019804,-0,-2.66318,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000020",
"vertices":[-1.04052,0,1.92489,-1.02496,2.80861,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,-1.03078,1.75692,0.158219,-1.03469,1.05241,0.866636,-1.03476,1.04039,-2.67734,-1.03072,1.76873,-1.94495,-1.04052,0,1.92489,1.78368,-0,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,0.725862,-0,-1.95712,0.019804,-0,-2.66318,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000040",
"vertices":[-1.04052,0,1.92489,1.78367,-0.004724,-0.899306,-1.04052,0.015564,-3.70794,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,0.008049,-2.66754,0.738025,0.002788,-1.93919,-1.04052,0,1.92489,1.78368,-0,-0.899306,-1.04052,0.015564,-3.70794,-1.04052,-0,-0.899306,0.725862,0.00583,-1.95129,0.019804,0.009721,-2.65346,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000060",
"vertices":[-1.04052,0,1.92489,1.78367,-0.004724,-0.899306,-1.04052,2.80863,-0.883742,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,1.76648,-0.889507,0.738025,1.03691,-0.893544,-1.04052,0,1.92489,1.78368,-0,-0.899306,-1.04052,2.80863,-0.883742,-1.04052,-0,-0.899306,0.725862,1.05198,-0.893477,0.019804,1.75415,-0.889585,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000080",
"vertices":[-1.04052,0,1.92489,1.77761,0.004499,-0.896183,-1.04052,9.5e-05,1.92489,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,-0.00169,0.878726,0.738025,-0.00294,0.146342,-1.04052,0,1.92489,1.77776,0.009366,-0.896172,-1.04052,9.5e-05,1.92489,-1.04052,-0,-0.899306,0.725862,3.5e-05,0.158508,0.019804,5.9e-05,0.864566,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000100",
"vertices":[-1.04052,0,1.92489,0.682133,1.65966,-0.323971,-1.04052,9.5e-05,1.92489,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,-0.00169,0.878726,0.738025,-0.00294,0.146342,-1.04052,0,1.92489,0.708388,1.69021,-0.321824,-1.04052,9.5e-05,1.92489,-1.04052,-0,-0.899306,0.725862,3.5e-05,0.158508,0.019804,5.9e-05,0.864566,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000120",
"vertices":[-1.03749,0.009238,1.91812,0.243204,0.007408,1.22895,-1.03748,0.009582,1.9178,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,-0.00169,0.878726,0.738025,-0.00294,0.146342,-1.03766,0.009582,1.91789,0.241797,0.007381,1.24887,-1.03749,0.009471,1.91768,-1.04052,-0,-0.899306,0.725862,3.5e-05,0.158508,0.019804,5.9e-05,0.864566,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000140",
"vertices":[-0.481006,1.66734,0.695937,0.243204,0.007408,1.22895,-0.478381,1.7127,0.636911,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,-0.00169,0.878726,0.738025,-0.00294,0.146342,-0.51189,1.72963,0.653594,0.241797,0.007381,1.24887,-0.480098,1.6926,0.615886,-1.04052,-0,-0.899306,0.725862,3.5e-05,0.158508,0.019804,5.9e-05,0.864566,0.004008,0,0.880361,0.73626,0,0.14811]
},{
"name":"animation_000160",
"vertices":[1.29201,0.073208,0.499518,0.243204,0.007408,1.22895,1.3241,0.104109,0.501091,-1.04052,-0,-0.899306,0.726148,-0.002955,0.158219,0.017732,-0.00177,0.866636,0.005643,-0.00169,0.878726,0.738025,-0.00294,0.146342,1.31646,0.105782,0.485952,0.241797,0.007381,1.24887,1.30659,0.086137,0.499062,-1.04052,-0,-0.899306,0.725862,3.5e-05,0.158508,0.019804,5.9e-05,0.864566,0.004008,0,0.880361,0.73626,0,0.14811]
}],
"faces":[40,1,3,7,0,1,2,0,0,0,40,3,4,5,1,3,4,1,1,1,40,4,3,1,3,1,0,0,0,0,40,5,0,3,4,5,1,0,0,0,40,6,3,2,6,1,7,2,2,2,40,7,3,6,2,1,6,3,3,3,40,8,14,11,5,8,1,4,4,4,40,10,11,13,7,1,9,5,5,5,40,12,11,9,10,1,11,6,6,6,40,13,11,12,9,1,10,7,7,7,40,11,14,15,1,8,12,8,8,8,40,15,9,11,12,11,1,2,2,2],
"vertices":[-1.04052,0,1.92489,-3.86472,-0,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,-2.80719,0,0.158219,-2.09877,0,0.866636,-2.08668,-0,-2.67734,-2.81907,-0,-1.94495,-1.04052,0,1.92489,1.78368,-0,-0.899306,-1.04052,-0,-3.7235,-1.04052,-0,-0.899306,0.725862,-0,-1.95712,0.019804,-0,-2.66318,0.004008,0,0.880361,0.73626,0,0.14811]
};
/**
* An enum of events that are fired from this class.
* @enum {String}
*/
Snowflake.Events = {
FOLDING_ANIMATION_COMPLETE: 'folding-animation-complete'
};
// SNOWFLAKE TEXTURE
/**
* Manages the creation of the texture using a 2D canvas.
*
* @constructor
*/
var SnowflakeTexture = function() {
/**
* Store the HTML canvas element.
* @type {HTMLCanvasElement}
* @private
*/
this.canvas_ = null;
};
/**
* Create the texture.
* @param {HTMLCanvasElement} halfSegment - Half the segment.
* @return {HTMLCanvasElement}
*/
SnowflakeTexture.prototype.createTexture = function(halfSegment) {
// Create a whole segment from half.
var segment = this.createSegment_(halfSegment);
this.canvas_ = document.createElement('canvas');
var context = this.canvas_.getContext('2d');
// Double the size of the canvas to make sure it has enough space.
this.canvas_.width = segment.height * 2;
this.canvas_.height = segment.height * 2;
var numSegments = 6;
context.translate((this.canvas_.width / 2) - (340 / 2), 0);
// Draw all of the segments.
for (var i = 0; i < numSegments; i++) {
var angle = (360 / numSegments) * i;
context.save();
context.translate(segment.width / 2, segment.height);
context.rotate(angle * Math.PI / 180);
context.translate(-(segment.width / 2), -segment.height);
context.drawImage(segment, 0, 2);
context.restore();
}
return this.canvas_;
};
/**
* This method takes half a segment and flips horizontally, rotates so both
* segments are merged together seamlessly. This also deals with rotating the
* whole segment so it's easy to loop when it comes to creating the full texture.
* @param {HTMLCanvasElement} halfSegment Pass the html5 canvas element of the half segment.
* @returns {HTMLCanvasElement} a canvas with a full segment.
* @private
*/
SnowflakeTexture.prototype.createSegment_ = function(halfSegment) {
var segmentCanvas = document.createElement('canvas');
var segmentContext = segmentCanvas.getContext('2d');
var trimmedImage = document.createElement('canvas');
var trimmedImageCtx = trimmedImage.getContext('2d');
trimmedImage.width = CuttingStage.FULLSIZE_CANVAS.width + 16;
trimmedImage.height = CuttingStage.FULLSIZE_CANVAS.height;
// Draw the half segment onto a trimming canvas, then trim the transparent pixels.
trimmedImageCtx.drawImage(halfSegment, 0, 0, trimmedImage.width, trimmedImage.height);
trimmedImage = this.cropImageFromCanvas_(trimmedImageCtx, trimmedImage);
segmentContext.save();
// Make the segment canvas double the size of the half segment, for room to double the image and rotate.
segmentCanvas.width = trimmedImage.width * 2;
segmentCanvas.height = trimmedImage.height * 2;
// Draw half the segment to 0, 0.
segmentContext.drawImage(trimmedImage, 0, 0);
// Translate the context to the top right position of the half segment.
segmentContext.translate(trimmedImage.width, 0);
// Rotate the canvas enough so the other half segment will draw next to it.
segmentContext.rotate(33.3 * Math.PI / 180);
// Translate the canvas back.
segmentContext.translate(-trimmedImage.width, 0);
// Translate the context to the opposite side of the canvas and scale to flip horizontally.
segmentContext.translate((trimmedImage.width * 2) - 1, 0);
segmentContext.scale(-1, 1);
// Draw the other side of the segment.
segmentContext.drawImage(trimmedImage, 1, 0);
segmentContext.restore();
// Create a temporary canvas to store the current state of the segment,
// used at a later stage to rotate the segment.
var tempCanvas = document.createElement('canvas');
var tempContext = tempCanvas.getContext('2d');
tempCanvas.width = segmentCanvas.width;
tempCanvas.height = segmentCanvas.height;
tempContext.drawImage(segmentCanvas, 0, 0);
// Clear the segment canvas ready for the next draw.
segmentContext.clearRect(0, 0, segmentCanvas.width, segmentCanvas.height);
// Rotate the canvas so the segment can be placed correctly.
segmentContext.rotate(16.5 * Math.PI / 180);
// Redraw the segment in the rotated position.
segmentContext.drawImage(tempCanvas, 0, 0);
// Trim the transparent pixels.
segmentCanvas = this.cropImageFromCanvas_(segmentContext, segmentCanvas);
return segmentCanvas;
};
/**
* Method for trimming the transparent pixels from a canvas.
* See http://stackoverflow.com/questions/11796554/automatically-crop-html5-canvas-to-contents
*
* @param {CanvasRenderingContext2D} ctx the canvas context to trim
* @param {HTMLCanvasElement} canvas The canvas to trim.
* @return {HTMLCanvasElement} Returns the trimmed canvas.
* @private
*/
SnowflakeTexture.prototype.cropImageFromCanvas_ = function(ctx, canvas) {
var w = canvas.width,
h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = pix.x[n] - pix.x[0];
h = pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
return canvas;
};
// CUTTING AREA
/**
* Manages the cutting stage of the process. Loads in 2 2D canvases, one for
* drawing the segment and one for displaying the marching cubes.
*
* @constructor
*/
var CuttingStage = function() {
/**
* The segment canvas, renders the paper texture with the cutouts.
* @type {HTMLCanvasElement}
* @private
*/
this.segmentCanvas_ = null;
/**
* Segment context, used for making draw calls.
* @type {CanvasRenderingContext2D}
* @private
*/
this.segmentContext_ = null;
/**
* The canvas element which shows the marching ants.
* @type {HTMLCanvasElement}
* @private
*/
this.marchingAntsCanvas_ = null;
/**
* Marching ants context, used for making draw calls.
* @type {CanvasRenderingContext2D}
* @private
*/
this.marchingAntsContext_ = null;
/**
* A bit of a hack, this is the width / height ratio of the segment canvas.
* Used for getting precise canvas size for the segment.
* @type {number}
* @private
*/
this.sizeRatio_ = 0.49333333333333335;
/**
* Flag to tell if the user is currently drawing or not. Used to choose
* between using lineTo or moveTo.
* @type {boolean}
*/
this.drawing = false;
/**
* Stores the initial point from when the user starts drawing from.
* @type {Object}
* @private
*/
this.startPoint_ = null;
/**
* An array of points to store the lines the user has made.
* @type {Array}
*/
this.lines = null;
/**
* Store the number of points in the current draw.
* @type {number}
* @private
*/
this.numPoints_ = 0;
/**
* An object which stores methods that are bound to this. Makes it easy to add
* and remove listeners.
* @type {Object}
* @private
*/
this.bindings_ = null;
/**
* Store the last time the ants were drawn.
* @type {number}
* @private
*/
this.lastTime_ = 0;
/**
* Store the ant offset. This is incremented in the update method to give the
* impression of marching ants.
* @type {number}
* @private
*/
this.antOffset_ = 0;
/**
* A scale factor between 0 and 1. Calculated by taking the current canvas
* scale and the full size canvas.
* @type {number}
* @private
*/
this.canvasScale_ = 1;
/**
* Cache the reference to the next button.
* @type {Element}
* @private
*/
this.nextButton_ = null;
/**
* The cursor element.
* @type {Element}
* @private
*/
this.cursor_ = null;
/**
* Store all of the lines arrays. This will be used to redraw all the cutouts
* if the user resizes the window.
* @type {Array}
* @private
*/
this.allLines_ = [];
};
// Extend the THREE.EventDispatcher prototype, to allow events to be fired from
// this class.
Object.assign(CuttingStage.prototype, THREE.EventDispatcher.prototype);
/**
* Create the marching ants canvas and add it to the document body.
* @private
*/
CuttingStage.prototype.createMarchingAntsCanvas_ = function() {
this.marchingAntsCanvas_ = document.createElement('canvas');
this.marchingAntsContext_ = this.marchingAntsCanvas_.getContext('2d');
this.marchingAntsCanvas_.classList.add('ants-canvas');
this.marchingAntsCanvas_.width = window.innerWidth;
this.marchingAntsCanvas_.height = window.innerHeight;
// Marching ants line config.
this.marchingAntsContext_.setLineDash([7]);
this.marchingAntsContext_.lineJoin = 'round';
this.marchingAntsContext_.strokeStyle = '#000';
this.marchingAntsContext_.lineWidth = '3';
document.body.appendChild(this.marchingAntsCanvas_);
};
/**
* Create the segment canvas, add it to the document body.
* @private
*/
CuttingStage.prototype.createSegmentCanvas_ = function() {
this.segmentCanvas_ = document.createElement('canvas');
this.segmentCanvas_.classList.add('segment-canvas');
this.segmentContext_ = this.segmentCanvas_.getContext('2d');
document.body.appendChild(this.segmentCanvas_);
// Wait for next frame to render on screen before taking measurements.
requestAnimationFrame(function() {
this.resize();
this.loadAndRenderSegmentPaper_();
// Add a class to the body to trigger a transition between the 3d and 2d
// canvas elements.
document.body.classList.add('cutting-mode');
// After a second, run the demo animation to make the inital cut.
setTimeout(function() {
this.makeInitialCut_(true);
}.bind(this), 1000);
}.bind(this));
};
/**
* Load the paper texture,
* @type {Function} callback - an optional callback to notify when the paper
* is drawn.
* @private
*/
CuttingStage.prototype.loadAndRenderSegmentPaper_ = function(callback) {
this.segmentContext_.save();
this.segmentContext_.fillStyle = "#f00";
this.segmentContext_.beginPath();
if (!isIOS()) {
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function() {
this.renderSegmentPaper_(image, callback);
}.bind(this);
image.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/934290/paper-tex2.png';
} else {
this.renderSegmentPaper_(null, callback);
}
};
/**
* Once image is loaded, draw and create a clipping mask so only the triangle
* shape is rendered.
* @param {Image} image The image to render
* @param {Function} callback
*/
CuttingStage.prototype.renderSegmentPaper_ = function(image, callback) {
if (image) {
// After the image has loaded, draw the paper to the canvas
this.segmentContext_.drawImage(image, 0, 0, this.segmentCanvas_.width, this.segmentCanvas_.height);
} else {
this.segmentContext_.fillStyle = '#fff';
this.segmentContext_.fillRect(0, 0, this.segmentCanvas_.width, this.segmentCanvas_.height);
}
// Set the globalCompositeOperation so future draws will mask the paper.
this.segmentContext_.globalCompositeOperation = 'destination-in';
// The positions to make the folded paper shape.
var positions = [{
x: 0,
y: 55
}, {
x: 127,
y: 619
}, {
x: 300,
y: 10
}, {
x: 122,
y: 110
}];
// Draw the paper shape.
this.segmentContext_.moveTo(positions[0].x * this.canvasScale_, positions[0].y * this.canvasScale_);
//
for (var i = 1; i < positions.length; i++) {
this.segmentContext_.lineTo(positions[i].x * this.canvasScale_, positions[i].y * this.canvasScale_);
}
this.segmentContext_.closePath();
this.segmentContext_.fill();
if (callback) {
callback.call(this);
}
};
/**
* Close off the path and draw finished path to the segment canvas.
* Reset the drawing property, ready for a new draw.
* @private
*/
CuttingStage.prototype.finishPath_ = function() {
if (!this.startPoint_) {
this.startPoint_ = this.lines[0];
}
this.marchingAntsContext_.lineTo(this.startPoint_.x, this.startPoint_.y);
this.marchingAntsContext_.stroke();
this.lines.push({
x: this.startPoint_.x,
y: this.startPoint_.y
});
// Convert the line positions so they're relative to the segment canvas.
var segmentCanvasPosition = this.segmentCanvas_.getBoundingClientRect();
var lines = this.lines.map(function(l) {
var line = {};
line.x = l.x - segmentCanvasPosition.left;
line.y = l.y - segmentCanvasPosition.top;
return line;
});
// Draw the cutout.
this.drawCutout_(lines);
this.allLines_.push(lines);
// Reset the marching ants, clear, and remove listeners
this.resetMarchingAnts_();
this.drawing = false;
};
/**
* Draw the cutout to the canvas to cutout areas of the segment.
* @param lines
* @private
*/
CuttingStage.prototype.drawCutout_ = function(lines) {
// Draw to the segment canvas. globalCompositeOperation set to
// destination-out, so these fills will delete parts of the segment.
this.segmentContext_.globalCompositeOperation = 'destination-out';
this.segmentContext_.beginPath();
this.segmentContext_.moveTo(lines[0].x, lines[0].y);
for (var i = 1; i < lines.length; i++) {
this.segmentContext_.lineTo(lines[i].x, lines[i].y);
}
this.segmentContext_.fill();
};
/**
* Reset the marching ants, clear, and remove listeners.
* @private
*/
CuttingStage.prototype.resetMarchingAnts_ = function() {
// Reset the marching ants, clear, and remove listeners
this.marchingAntsContext_.clearRect(0, 0, this.marchingAntsCanvas_.width, this.marchingAntsCanvas_.height);
this.marchingAntsCanvas_.removeEventListener('mousemove', this.bindings_.onMouseMove, false);
this.marchingAntsCanvas_.removeEventListener('dblclick', this.bindings_.onDoubleClick, false);
document.removeEventListener('keydown', this.bindings_.onKeyPress, false);
this.drawing = false;
};
/**
* In the folding process, the end part is to chop diagonally across the top of
* the segment. This is done to produce the 6 sided snowflake.
* This method makes that initial cut.
*
* @param {boolean} animate If truthy, an animation will be played out to show
* the user how to cut.
* @private
*/
CuttingStage.prototype.makeInitialCut_ = function(animate) {
// The positions for the initial cut.
var lines = [{
x: 277,
y: 123
}, {
x: 1,
y: 283
}, {
x: -110,
y: 10
}, {
x: 341,
y: -47
}, {
x: 267,
y: 136
}];
if (animate) {
// Initiate the demo animation.
var animation = new DemoAnimation(this, lines, this.canvasScale_);
animation.init();
animation.addEventListener(DemoAnimation.Events.ANIMATION_COMPLETE, this.bindings_.demoComplete, false );
} else {
// Just draw the cutout.
var scaledLines = this.scaleLines_(lines);
this.drawCutout_(scaledLines);
}
};
/**
* When the demo animation completes, finish the path, and add the listeners to
* interact with the canvas.
* @private
*/
CuttingStage.prototype.onDemoComplete_ = function() {
this.finishPath_();
document.addEventListener('mousemove', this.bindings_.updateCursor, false);
this.marchingAntsCanvas_.addEventListener('mousedown', this.bindings_.onMouseDown, false);
};
/**
* If the user is not drawing, set a new start point and reset drawing stats.
* If a draw is currently active, update the draw stats with the latest mouse
* positions.
* @param {Event} e Mouse event object.
* @private
*/
CuttingStage.prototype.onMouseDown_ = function(e) {
// If the user is not drawing yet (i.e. the path has not started), set the
// start point, set drawing to true, and add listeners to track the mouse
// movements and key presses.
if (!this.drawing) {
this.startPoint_ = {
x: e.clientX,
y: e.clientY
};
this.drawing = true;
this.lines = [this.startPoint_];
this.numPoints_ = 1;
this.marchingAntsCanvas_.addEventListener('mousemove', this.bindings_.onMouseMove, false);
this.marchingAntsCanvas_.addEventListener('dblclick', this.bindings_.onDoubleClick, false);
document.addEventListener('keydown', this.bindings_.onKeyPress, false);
// Start the draw cycles.
this.update();
} else {
// If the user is already drawing, update the line.
this.marchingAntsContext_.lineTo(e.clientX, e.clientY);
var point = {
x: e.clientX,
y: e.clientY
};
this.lines.push(point);
this.numPoints_ ++;
// If the click is within 10 pixels of the start point on both the x and y
// axis, assume the user wants to close the path.
if (Math.abs(e.clientX - this.startPoint_.x) < 10 &&
Math.abs(e.clientY - this.startPoint_.y) < 10) {
this.finishPath_();
}
}
};
/**
* If drawing, update the latest line to the current mouse position. So the
* marching ants line can move with the mouse.
* @param {Event} e The mouse event object.
* @private
*/
CuttingStage.prototype.onMouseMove_ = function(e) {
if (this.drawing) {
this.lines[this.numPoints_] = {
x: e.clientX,
y: e.clientY
};
}
};
/**
* If the user double clicked, close off the path.
* @private
*/
CuttingStage.prototype.onDoubleClick_ = function() {
if (this.drawing) {
this.finishPath_();
}
};
/**
* If the user hits the escape key, cancel the current path.
* @param {KeyboardEvent} e
* @private
*/
CuttingStage.prototype.onKeyPress_ = function(e) {
if (e.keyCode === 27) { // Escape key
this.resetMarchingAnts_();
}
};
/**
* Draw the marching ants for the current draw positions. Only repeat the draw
* call if the drawing flag is set to true.
*/
CuttingStage.prototype.update = function() {
if (this.drawing) {
if (this.lines.length > 0) {
var currentTime = Date.now();
// Only update the ant offsets every 20 ms.
if (currentTime - this.lastTime_ > 20) {
this.antOffset_ ++;
this.lastTime_ = currentTime;
}
this.marchingAntsContext_.clearRect(0, 0, this.marchingAntsCanvas_.width, this.marchingAntsCanvas_.height);
// Draw the marching ants line.
this.marchingAntsContext_.lineDashOffset = this.antOffset_;
this.marchingAntsContext_.beginPath();
this.marchingAntsContext_.moveTo(this.lines[0].x, this.lines[0].y);
this.lines.forEach(function(line) {
this.marchingAntsContext_.lineTo(line.x, line.y);
}.bind(this));
this.marchingAntsContext_.stroke();
}
// Only request another update if the user is still drawing.
requestAnimationFrame(this.update.bind(this));
}
};
/**
* Fired when the user clicks on the "Next" button. Dispatch an event to notify
* parent that this area is complete.
* @private
*/
CuttingStage.prototype.finishCutting_ = function() {
document.body.classList.remove('cutting-mode');
this.dispatchEvent({
type: CuttingStage.Events.COMPLETED,
segment: this.segmentCanvas_
});
};
/**
* On mouse move, update the cursor graphic. The reason why I'm doing this with
* JS rather than using the cursor css property, is because I wanted to use a
* blend mode on the cursor.
* @param {MouseEvent} e
* @private
*/
CuttingStage.prototype.updateCursorPosition_ = function(e) {
this.cursor_.style.transform = 'translate3d(' + e.clientX + 'px, ' + e.clientY + 'px, 0)';
};
/**
* Resize the 2d canvas elements.
*/
CuttingStage.prototype.resize = function() {
var height = this.segmentCanvas_.offsetHeight;
var width = height * this.sizeRatio_;
this.segmentCanvas_.style.width = width + 'px';
this.segmentCanvas_.width = width;
this.segmentCanvas_.height = height;
this.canvasScale_ = this.segmentCanvas_.height / CuttingStage.FULLSIZE_CANVAS.height;
};
/**
* When the browser is resized, resize the canvas element and re-render the
* paper and lines.
*/
CuttingStage.prototype.onResize = debounce(function() {
this.resize();
this.rerender_();
}, 500);
/**
* Re-render the paper segment, along with the initial cut and lines.
* This method is only called if the browser is resized.
* @private
*/
CuttingStage.prototype.rerender_ = function() {
this.segmentContext_.clearRect(0, 0, this.segmentCanvas_.width, this.segmentCanvas_.height);
this.segmentContext_.restore();
this.loadAndRenderSegmentPaper_(function() {
this.makeInitialCut_(false);
this.allLines_.forEach(function(lines) {
var scaledLines = this.scaleLines_(lines);
this.drawCutout_(scaledLines);
}.bind(this));
}.bind(this));
};
/**
* Scale a point's position by the scale of the canvas.
* @param {Array} lines
* @return {Array}
* @private
*/
CuttingStage.prototype.scaleLines_ = function(lines) {
return lines.map(function(line) {
return {
x: line.x * this.canvasScale_,
y: line.y * this.canvasScale_
}
}.bind(this));
};
/**
* Initiate the cutting stage, create the segment canvas, marching ants canvas,
* and cache the elements that are needed at this stage.
*/
CuttingStage.prototype.init = function() {
// Store the event listener bindings, this makes it easier to add and remove
// listeners, while maintaining scope.
this.bindings_ = {
onMouseDown: this.onMouseDown_.bind(this),
onMouseMove: this.onMouseMove_.bind(this),
onDoubleClick: this.onDoubleClick_.bind(this),
onKeyPress: this.onKeyPress_.bind(this),
onNextClicked: this.finishCutting_.bind(this),
updateCursor: this.updateCursorPosition_.bind(this),
demoComplete: this.onDemoComplete_.bind(this)
};
this.createSegmentCanvas_();
this.createMarchingAntsCanvas_();
this.nextButton_ = document.getElementById('js-next-button');
this.nextButton_.addEventListener('click', this.bindings_.onNextClicked, false);
this.cursor_ = document.getElementById('cursor');
};
/**
* Kill the cutting stage, remove listeners, nullify variables.
*/
CuttingStage.prototype.destroy = function() {
this.marchingAntsContext_.clearRect(0, 0, this.marchingAntsCanvas_.width, this.marchingAntsCanvas_.height);
this.marchingAntsCanvas_.removeEventListener('mousemove', this.bindings_.onMouseMove, false);
this.marchingAntsCanvas_.removeEventListener('dblclick', this.bindings_.onDoubleClick, false);
document.removeEventListener('keydown', this.bindings_.onKeyPress, false);
this.segmentCanvas_.parentNode.removeChild(this.segmentCanvas_);
this.marchingAntsCanvas_.parentNode.removeChild(this.marchingAntsCanvas_);
document.removeEventListener('mousemove', this.bindings_.updateCursor, false);
this.cursor_ = null;
this.segmentCanvas_ = null;
this.segmentContext_ = null;
this.marchingAntsCanvas_ = null;
this.marchingAntsContext_ = null;
this.drawing = false;
this.startPoint_ = null;
this.lines = null;
this.numPoints_ = 0;
this.bindings_ = null;
this.lastTime_ = 0;
this.antOffset_ = 0;
this.canvasScale_ = 1;
};
/**
* An enum of events that are fired from this class.
* @enum {String}
*/
CuttingStage.Events = {
COMPLETED: 'cutting-stage-complete'
};
/**
* Store the full size canvas dimensions. Used to calculate the scale.
* @type {{width: number, height: number}}
*/
CuttingStage.FULLSIZE_CANVAS = {
width: 305,
height: 619
};
// DEMO ANIMATION
/**
* Class that controls the demo animation within the cutting stage.
*
* @param {CuttingStage} cuttingArea - Reference to the cutting area.
* @param {Array} points - The array of points to animate.
* @param {number} scale - The canvas scale.
* @constructor
*/
var DemoAnimation = function(cuttingArea, points, scale) {
/**
* Store the cutting area reference.
* @type {CuttingStage}
* @private
*/
this.cuttingArea_ = cuttingArea;
/**
* The touch element.
* @type {Element}
* @private
*/
this.touchMarker_ = null;
/**
* The cursor element.
* @type {Element}
* @private
*/
this.cursor_ = null;
/**
* The TimelineMax timeline.
* @type {TimelineMax}
* @private
*/
this.timeline_ = null;
/**
* The array of points to animate between.
* @type {Array}
* @private
*/
this.points_ = points;
/**
* Store the canvas scale
* @type {number}
* @private
*/
this.canvasScale_ = scale;
};
// Extend the THREE.EventDispatcher prototype, to allow events to be fired from
// this class.
Object.assign(DemoAnimation.prototype, THREE.EventDispatcher.prototype);
/**
* Setup the animation.
* @private
*/
DemoAnimation.prototype.setup_ = function() {
this.timeline_ = new TimelineMax({
align: 'sequence',
onComplete: this.complete_.bind(this)
});
// Convert points relative to canvas.
var worldPoints = this.getWorldPoints_(this.points_);
// Set the properties on the CuttingStage to allow the marching ants to start.
this.cuttingArea_.drawing = true;
this.cuttingArea_.lines = [];
this.cuttingArea_.update();
// Setup the steps on the timeline.
for (var i = 0; i < worldPoints.length; i++) {
this.timeline_.to(this.cursor_, 0.5, {
x: worldPoints[i].x,
y: worldPoints[i].y
});
this.timeline_.set(this.touchMarker_, {
x: worldPoints[i].x,
y: worldPoints[i].y,
opacity: 1,
scale: 0
});
this.timeline_.to(this.touchMarker_, 0.5, {
scale: 1,
opacity: 0
});
this.timeline_.addCallback(this.simulateTouch_, null, [worldPoints[i]], this);
}
};
/**
* Convert the points which are relative to the canvas to world points.
* @param {Array} points
* @return {Array}
* @private
*/
DemoAnimation.prototype.getWorldPoints_ = function(points) {
var canvasRect = this.cuttingArea_.segmentCanvas_.getBoundingClientRect();
return points.map(function(point) {
return {
x: canvasRect.left + (point.x * this.canvasScale_),
y: canvasRect.top + (point.y * this.canvasScale_)
};
}.bind(this));
};
/**
* Push the point to the lines array.
* @param point
* @private
*/
DemoAnimation.prototype.simulateTouch_ = function(point) {
this.cuttingArea_.lines.push(point);
};
/**
* When the demo animation completes, fire an event.
* @private
*/
DemoAnimation.prototype.complete_ = function() {
this.dispatchEvent({
type: DemoAnimation.Events.ANIMATION_COMPLETE
});
};
/**
* Get the dom elements that are needed and run setup.
*/
DemoAnimation.prototype.init = function() {
this.touchMarker_ = document.getElementById('touch-marker');
this.cursor_ = document.getElementById('cursor');
this.setup_();
};
/**
* An enum of events that are fired from this class.
* @enum {String}
*/
DemoAnimation.Events = {
ANIMATION_COMPLETE: 'demo-animation-complete'
};
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author erich666 / http://erichaines.com
*/
// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
// Orbit - left mouse / touch: one finger move
// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
// Pan - right mouse, or arrow keys / touch: three finter swipe
THREE.OrbitControls = function ( object, domElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new THREE.Vector3();
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0;
this.maxDistance = Infinity;
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0;
this.maxZoom = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
this.minAzimuthAngle = - Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.25;
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
// Set to false to disable use of the keys
this.enableKeys = true;
// The four arrow keys
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
// Mouse buttons
this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.reset = function () {
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent( changeEvent );
scope.update();
state = STATE.NONE;
};
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function() {
var offset = new THREE.Vector3();
// so camera.up is the orbit axis
var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
var quatInverse = quat.clone().inverse();
var lastPosition = new THREE.Vector3();
var lastQuaternion = new THREE.Quaternion();
return function update () {
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion( quat );
// angle from z-axis around y-axis
spherical.setFromVector3( offset );
if ( scope.autoRotate && state === STATE.NONE ) {
rotateLeft( getAutoRotationAngle() );
}
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
// restrict theta to be between desired limits
spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
// restrict phi to be between desired limits
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
spherical.makeSafe();
spherical.radius *= scale;
// restrict radius to be between desired limits
spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
// move target to panned location
scope.target.add( panOffset );
offset.setFromSpherical( spherical );
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion( quatInverse );
position.copy( scope.target ).add( offset );
scope.object.lookAt( scope.target );
if ( scope.enableDamping === true ) {
sphericalDelta.theta *= ( 1 - scope.dampingFactor );
sphericalDelta.phi *= ( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
}
scale = 1;
panOffset.set( 0, 0, 0 );
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if ( zoomChanged ||
lastPosition.distanceToSquared( scope.object.position ) > EPS ||
8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
lastQuaternion.copy( scope.object.quaternion );
zoomChanged = false;
return true;
}
return false;
};
}();
this.dispose = function() {
scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
window.removeEventListener( 'keydown', onKeyDown, false );
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
};
//
// internals
//
var scope = this;
var changeEvent = { type: 'change' };
var startEvent = { type: 'start' };
var endEvent = { type: 'end' };
var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
var state = STATE.NONE;
var EPS = 0.000001;
// current position in spherical coordinates
var spherical = new THREE.Spherical();
var sphericalDelta = new THREE.Spherical();
var scale = 1;
var panOffset = new THREE.Vector3();
var zoomChanged = false;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var panStart = new THREE.Vector2();
var panEnd = new THREE.Vector2();
var panDelta = new THREE.Vector2();
var dollyStart = new THREE.Vector2();
var dollyEnd = new THREE.Vector2();
var dollyDelta = new THREE.Vector2();
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow( 0.95, scope.zoomSpeed );
}
function rotateLeft( angle ) {
sphericalDelta.theta -= angle;
}
function rotateUp( angle ) {
sphericalDelta.phi -= angle;
}
var panLeft = function() {
var v = new THREE.Vector3();
return function panLeft( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
v.multiplyScalar( - distance );
panOffset.add( v );
};
}();
var panUp = function() {
var v = new THREE.Vector3();
return function panUp( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
v.multiplyScalar( distance );
panOffset.add( v );
};
}();
// deltaX and deltaY are in pixels; right and down are positive
var pan = function() {
var offset = new THREE.Vector3();
return function pan ( deltaX, deltaY ) {
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
if ( scope.object instanceof THREE.PerspectiveCamera ) {
// perspective
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we actually don't use screenWidth, since perspective camera is fixed to screen height
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
// orthographic
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
} else {
// camera neither orthographic nor perspective
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
scope.enablePan = false;
}
};
}();
function dollyIn( dollyScale ) {
if ( scope.object instanceof THREE.PerspectiveCamera ) {
scale /= dollyScale;
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function dollyOut( dollyScale ) {
if ( scope.object instanceof THREE.PerspectiveCamera ) {
scale *= dollyScale;
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate( event ) {
//console.log( 'handleMouseDownRotate' );
rotateStart.set( event.clientX, event.clientY );
}
function handleMouseDownDolly( event ) {
//console.log( 'handleMouseDownDolly' );
dollyStart.set( event.clientX, event.clientY );
}
function handleMouseDownPan( event ) {
//console.log( 'handleMouseDownPan' );
panStart.set( event.clientX, event.clientY );
}
function handleMouseMoveRotate( event ) {
//console.log( 'handleMouseMoveRotate' );
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
// rotating across whole screen goes 360 degrees around
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
// rotating up and down along whole screen attempts to go 360, but limited to 180
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleMouseMoveDolly( event ) {
//console.log( 'handleMouseMoveDolly' );
dollyEnd.set( event.clientX, event.clientY );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyIn( getZoomScale() );
} else if ( dollyDelta.y < 0 ) {
dollyOut( getZoomScale() );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleMouseMovePan( event ) {
//console.log( 'handleMouseMovePan' );
panEnd.set( event.clientX, event.clientY );
panDelta.subVectors( panEnd, panStart );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleMouseUp( event ) {
//console.log( 'handleMouseUp' );
}
function handleMouseWheel( event ) {
//console.log( 'handleMouseWheel' );
if ( event.deltaY < 0 ) {
dollyOut( getZoomScale() );
} else if ( event.deltaY > 0 ) {
dollyIn( getZoomScale() );
}
scope.update();
}
function handleKeyDown( event ) {
//console.log( 'handleKeyDown' );
switch ( event.keyCode ) {
case scope.keys.UP:
pan( 0, scope.keyPanSpeed );
scope.update();
break;
case scope.keys.BOTTOM:
pan( 0, - scope.keyPanSpeed );
scope.update();
break;
case scope.keys.LEFT:
pan( scope.keyPanSpeed, 0 );
scope.update();
break;
case scope.keys.RIGHT:
pan( - scope.keyPanSpeed, 0 );
scope.update();
break;
}
}
function handleTouchStartRotate( event ) {
//console.log( 'handleTouchStartRotate' );
rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
}
function handleTouchStartDolly( event ) {
//console.log( 'handleTouchStartDolly' );
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyStart.set( 0, distance );
}
function handleTouchStartPan( event ) {
//console.log( 'handleTouchStartPan' );
panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
}
function handleTouchMoveRotate( event ) {
//console.log( 'handleTouchMoveRotate' );
rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
rotateDelta.subVectors( rotateEnd, rotateStart );
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
// rotating across whole screen goes 360 degrees around
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
// rotating up and down along whole screen attempts to go 360, but limited to 180
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleTouchMoveDolly( event ) {
//console.log( 'handleTouchMoveDolly' );
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyEnd.set( 0, distance );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyOut( getZoomScale() );
} else if ( dollyDelta.y < 0 ) {
dollyIn( getZoomScale() );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleTouchMovePan( event ) {
//console.log( 'handleTouchMovePan' );
panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
panDelta.subVectors( panEnd, panStart );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleTouchEnd( event ) {
//console.log( 'handleTouchEnd' );
}
//
// event handlers - FSM: listen for events and reset state
//
function onMouseDown( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( event.button === scope.mouseButtons.ORBIT ) {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
} else if ( event.button === scope.mouseButtons.ZOOM ) {
if ( scope.enableZoom === false ) return;
handleMouseDownDolly( event );
state = STATE.DOLLY;
} else if ( event.button === scope.mouseButtons.PAN ) {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
}
if ( state !== STATE.NONE ) {
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( startEvent );
}
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
if ( scope.enableRotate === false ) return;
handleMouseMoveRotate( event );
} else if ( state === STATE.DOLLY ) {
if ( scope.enableZoom === false ) return;
handleMouseMoveDolly( event );
} else if ( state === STATE.PAN ) {
if ( scope.enablePan === false ) return;
handleMouseMovePan( event );
}
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
handleMouseUp( event );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onMouseWheel( event ) {
if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
event.preventDefault();
event.stopPropagation();
handleMouseWheel( event );
scope.dispatchEvent( startEvent ); // not sure why these are here...
scope.dispatchEvent( endEvent );
}
function onKeyDown( event ) {
if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
handleKeyDown( event );
}
function onTouchStart( event ) {
if ( scope.enabled === false ) return;
switch ( event.touches.length ) {
case 1: // one-fingered touch: rotate
if ( scope.enableRotate === false ) return;
handleTouchStartRotate( event );
state = STATE.TOUCH_ROTATE;
break;
case 2: // two-fingered touch: dolly
if ( scope.enableZoom === false ) return;
handleTouchStartDolly( event );
state = STATE.TOUCH_DOLLY;
break;
case 3: // three-fingered touch: pan
if ( scope.enablePan === false ) return;
handleTouchStartPan( event );
state = STATE.TOUCH_PAN;
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( startEvent );
}
}
function onTouchMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
event.stopPropagation();
switch ( event.touches.length ) {
case 1: // one-fingered touch: rotate
if ( scope.enableRotate === false ) return;
if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
handleTouchMoveRotate( event );
break;
case 2: // two-fingered touch: dolly
if ( scope.enableZoom === false ) return;
if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
handleTouchMoveDolly( event );
break;
case 3: // three-fingered touch: pan
if ( scope.enablePan === false ) return;
if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
handleTouchMovePan( event );
break;
default:
state = STATE.NONE;
}
}
function onTouchEnd( event ) {
if ( scope.enabled === false ) return;
handleTouchEnd( event );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onContextMenu( event ) {
event.preventDefault();
}
//
scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
window.addEventListener( 'keydown', onKeyDown, false );
// force an update at start
this.update();
};
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
Object.defineProperties( THREE.OrbitControls.prototype, {
center: {
get: function () {
console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
return this.target;
}
},
// backward compatibility
noZoom: {
get: function () {
console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
return ! this.enableZoom;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
this.enableZoom = ! value;
}
},
noRotate: {
get: function () {
console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
return ! this.enableRotate;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
this.enableRotate = ! value;
}
},
noPan: {
get: function () {
console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
return ! this.enablePan;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
this.enablePan = ! value;
}
},
noKeys: {
get: function () {
console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
return ! this.enableKeys;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
this.enableKeys = ! value;
}
},
staticMoving : {
get: function () {
console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
return ! this.enableDamping;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
this.enableDamping = ! value;
}
},
dynamicDampingFactor : {
get: function () {
console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
return this.dampingFactor;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
this.dampingFactor = value;
}
}
} );
(function() {
// Systems are go!
var snowflake = new SnowflakeApp();
snowflake.init();
})();
Also see: Tab Triggers