js实现可拖拽旋转三维音乐卡通人物效果代码
代码语言:html
所属分类:三维
代码描述:js实现可拖拽旋转三维音乐卡通人物效果代码
下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开
<!doctype html> <html> <head> <meta charset="utf-8"> <title></title> <style> html { height: 100%; } body { min-height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: #435; color: white; font-family: sans-serif; text-align: center; } canvas { display: block; margin: 0 auto; cursor: move; } </style> </head> <body> <div class="container"> <canvas></canvas> <p>点击拖动或旋转</p> </div> <script> // -------------------------- utils -------------------------- // var TAU = Math.PI * 2; function extend( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; } function lerp( a, b, t ) { return ( b - a ) * t + a; } function modulo( num, div ) { return ( ( num % div ) + div ) % div; } // -------------------------- Vector3 -------------------------- // function Vector3( position ) { this.set( position ); } Vector3.prototype.set = function( pos ) { pos = Vector3.sanitize( pos ); this.x = pos.x; this.y = pos.y; this.z = pos.z; return this; }; Vector3.prototype.rotate = function( rotation ) { if ( !rotation ) { return; } this.rotateZ( rotation.z ); this.rotateY( rotation.y ); this.rotateX( rotation.x ); return this; }; Vector3.prototype.rotateZ = function( angle ) { rotateProperty( this, angle, 'x', 'y' ); }; Vector3.prototype.rotateX = function( angle ) { rotateProperty( this, angle, 'y', 'z' ); }; Vector3.prototype.rotateY = function( angle ) { rotateProperty( this, angle, 'x', 'z' ); }; function rotateProperty( vec, angle, propA, propB ) { if ( angle % TAU === 0 ) { return; } var cos = Math.cos( angle ); var sin = Math.sin( angle ); var a = vec[ propA ]; var b = vec[ propB ]; vec[ propA ] = a*cos - b*sin; vec[ propB ] = b*cos + a*sin; } Vector3.prototype.add = function( vec ) { if ( !vec ) { return; } vec = Vector3.sanitize( vec ); this.x += vec.x; this.y += vec.y; this.z += vec.z; return this; }; Vector3.prototype.multiply = function( vec ) { if ( !vec ) { return; } vec = Vector3.sanitize( vec ); this.x *= vec.x; this.y *= vec.y; this.z *= vec.z; return this; }; Vector3.prototype.lerp = function( vec, t ) { this.x = lerp( this.x, vec.x, t ); this.y = lerp( this.y, vec.y, t ); this.z = lerp( this.z, vec.z, t ); return this; }; // ----- utils ----- // // add missing properties Vector3.sanitize = function( vec ) { vec = vec || {}; vec.x = vec.x || 0; vec.y = vec.y || 0; vec.z = vec.z || 0; return vec; }; // -------------------------- PathAction -------------------------- // function PathAction( method, points, previousPoint ) { this.method = method; this.points = points.map( mapVectorPoint ); this.renderPoints = points.map( mapVectorPoint ); this.previousPoint = previousPoint; this.endRenderPoint = this.renderPoints[ this.renderPoints.length - 1 ]; // arc actions come with previous point & corner point // but require bezier control points if ( method == 'arc' ) { this.controlPoints = [ new Vector3(), new Vector3() ]; } } function mapVectorPoint( point ) { return new Vector3( point ); } PathAction.prototype.reset = function() { // reset renderPoints back to orignal points position var points = this.points; this.renderPoints.forEach( function( renderPoint, i ) { var point = points[i]; renderPoint.set( point ); }); }; PathAction.prototype.transform = function( translation, rotation, scale ) { this.renderPoints.forEach( function( renderPoint ) { renderPoint.multiply( scale ); renderPoint.rotate( rotation ); renderPoint.add( translation ); }); }; PathAction.prototype.render = function( ctx ) { this[ this.method ]( ctx ); }; PathAction.prototype.move = function( ctx ) { var point = this.renderPoints[0]; ctx.moveTo( point.x, point.y ); }; PathAction.prototype.line = function( ctx ) { var point = this.renderPoints[0]; ctx.lineTo( point.x, point.y ); }; PathAction.prototype.bezier = function( ctx ) { var cp0 = this.renderPoints[0]; var cp1 = this.renderPoints[1]; var end = this.renderPoints[2]; ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y ); }; PathAction.prototype.arc = function( ctx ) { var prev = this.previousPoint; var corner = this.renderPoints[0]; var end = this.renderPoints[1]; var cp0 = this.controlPoints[0]; var cp1 = this.controlPoints[1]; cp0.set( prev ).lerp( corner, 9/16 ); cp1.set( end ).lerp( corner, 9/16 ); ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y ); }; // -------------------------- Shape -------------------------- // function Shape( options ) { this.create( options ); } Shape.prototype.create = function( options ) { // default extend( this, Shape.defaults ); // set options setOptions( this, options ); this.updatePathActions(); // transform this.translate = new Vector3( options.translate ); this.rotate = new Vector3( options.rotate ); var scale = extend( { x: 1, y: 1, z: 1 }, options.scale ); this.scale = new Vector3( scale ); // children this.children = []; if ( this.addTo ) { this.addTo.addChild( this ); } }; Shape.defaults = { stroke: true, fill: false, color: 'black', lineWidth: 1, closed: true, rendering: true, path: [ {} ], }; var optionKeys = Object.keys( Shape.defaults ).concat([ 'rotate', 'translate', 'scale', 'addTo', 'width', 'height', ]); function setOptions( shape, options ) { for ( var key in options ) { if ( optionKeys.includes( key ) ) { shape[ key ] = options[ key ]; } } } var actionNames = [ 'move', 'line', 'bezier', 'arc', ]; // parse path into PathActions Shape.prototype.updatePathActions = function() { var previousPoint; this.pathActions = this.path.map( function( pathPart, i ) { // pathPart can be just vector coordinates -> { x, y, z } // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] } var keys = Object.keys( pathPart ); var method = keys[0]; var points = pathPart[ method ]; var isInstruction = keys.length === 1 && actionNames.includes( method ) && Array.isArray( points ); if ( !isInstruction ) { method = 'line'; points = [ pathPart ]; } // first action is always move method = i === 0 ? 'move' : method; // arcs require previous last point var pathAction = new PathAction( method, points, previousPoint ); // update previousLastPoint previousPoint = pathAction.endRenderPoint; return pathAction; }); }; Shape.prototype.addChild = function( shape ) { this.children.push( shape ); }; // ----- update ----- // Shape.prototype.update = function() { // update self this.reset(); // update children this.children.forEach( function( child ) { child.update(); }); this.transform( this.translate, this.rotate, this.scale ); }; Shape.prototype.reset = function() { // reset pathAction render points this.pathActions.forEach( function( pathAction ) { pathAction.reset(); }); }; Shape.prototype.transform = function( translation, rotation, scale ) { // transform points this.pathActions.forEach( function( pathAction ) { pathAction.transform( translation, rotation, scale ); }); // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); }); }; Shape.prototype.updateSortValue = function() { var sortValueTotal = 0; this.pathActions.forEach( function( pathAction ) { sortValueTotal += pathAction.endRenderPoint.z; }); // average sort value of all points // def not geometrically correct, but works for me this.sortValue = sortValueTotal / this.pathActions.length; }; // ----- render ----- // Shape.prototype.render = function( ctx ) { var length = this.pathActions.length; if ( !this.rendering || !length ) { return; } var isDot = length == 1; if ( isDot ) { this.renderDot( ctx ); } else { this.renderPath( ctx ); } }; // Safari does not render lines with no size, have to render circle instead Shape.prototype.renderDot = function( ctx ) { ctx.fillStyle = this.color; var point = this.pathActions[0].endRenderPoint; ctx.beginPath(); var radius = this.lineWidth/2; ctx.arc( point.x, point.y, radius, 0, TAU ); ctx.fill(); }; Shape.prototype.renderPath = function( ctx ) { // set render properties ctx.fillStyle = this.color; ctx.strokeStyle = this.color; ctx.lineWidth = this.lineWidth; // render points ctx.beginPath(); this.pathActions.forEach( function( pathAction ) { pathAction.render( ctx ); }); var isTwoPoints = this.pathActions.length == 2 && this.pathActions[1].method == 'line'; if ( !isTwoPoints && this.closed ) { ctx.closePath(); } if ( this.stroke ) { ctx.stroke(); } if ( this.fill ) { ctx.fill(); } }; // return Array of self & all child shapes Shape.prototype.getShapes = function() { var shapes = [ this ]; this.children.forEach( function( child ) { var childShapes = child.getShapes(); shapes = shapes.concat( childShapes ); }); return shapes; }; Shape.prototype.copy = function( options ) { // copy options var shapeOptions = {}; optionKeys.forEach( function( key ) { shapeOptions[ key ] = this[ key ]; }, this ); // add set options setOptions( shapeOptions, options ); var ShapeClass = this.constructor; return new ShapeClass( shapeOptions ); }; // -------------------------- Ellipse -------------------------- // function Ellipse( options ) { options = this.setPath( options ); // always keep open // fixes overlap bug when lineWidth is greater than radius options.closed = false; this.create( options ); } Ellipse.prototype = Object.create( Shape.prototype ); Ellipse.prototype.constructor = Ellipse; Ellipse.prototype.setPath = function( options ) { var w = options.width/2; var h = options.height/2; options.path = [ { x: 0, y: -h }, { arc: [ // top right { x: w, y: -h }, { x: w, y: 0 }, ]}, { arc: [ // bottom right { x: w, y: h }, { x: 0, y: h }, ]}, { arc: [ // bottom left { x: -w, y: h }, { x: -w, y: 0 }, ]}, { arc: [ // bottom left { x: -w, y: -h }, { x: 0, y: -h }, ]}, ]; return options; }; // -------------------------- Group -------------------------- // function Group( options ) { this.create( options ); } Group.prototype.create = function( options ) { // set options setGroupOptions( this, options ); // transform this.translate = Vector3.sanitize( this.translate ); this.rotate = Vector3.sanitize( this.rotate ); // children this.children = []; if ( this.addTo ) { this.addTo.addChild( this ); } }; var groupOptionKeys = [ 'rotate', 'translate', 'addTo', ]; function setGroupOptions( shape, options ) { for ( var key in options ) { if ( groupOptionKeys.includes( key ) ) { shape[ key ] = options[ key ]; } } } Group.prototype.addChild = function( shape ) { this.children.push( shape ); }; // ----- update ----- // Group.prototype.update = function() { // update self this.reset(); // update children this.children.forEach( function( child ) { child.update(); }); this.transform( this.translate, this.rotate, this.scale ); }; Group.prototype.reset = function() {}; Group.prototype.transform = function( translation, rotation, scale ) { // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); }); }; Group.prototype.updateSortValue = function() { var sortValueTotal = 0; this.children.forEach( function( child ) { child.updateSortValue(); sortValueTotal += child.sortValue; }); // TODO sort children? // average sort value of all points // def not geometrically correct, but works for me this.sortValue = sortValueTotal / this.children.length; }; // ----- render ----- // Group.prototype.render = function( ctx ) { this.children.forEach( function( child ) { child.render( ctx ); }); }; // do not include children, group handles rendering & sorting internally Group.prototype.getShapes = function() { return [ this ]; }; // -------------------------- Dragger -------------------------- // // quick & dirty drag event stuff // messes up if multiple pointers/touches // event support, default to mouse events var downEvent = 'mousedown'; var moveEvent = 'mousemove'; var upEvent = 'mouseup'; if ( window.PointerEvent ) { // PointerEvent, Chrome downEvent = 'pointerdown'; moveEvent = 'pointermove'; upEvent = 'pointerup'; } else if ( 'ontouchstart' in window ) { // Touch Events, iOS Safari downEvent = 'touchstart'; moveEvent = 'touchmove'; upEvent = 'touchend'; } function noop() {} function Dragger( options ) { this.startElement = options.startElement; this.onPointerDown = options.onPointerDown || noop; this.onPointerMove = options.onPointerMove || noop; this.onPointerUp = options.onPointerUp || noop; this.startElement.addEventListener( downEvent, this ); } Dragger.prototype.handleEvent = function( event ) { var method = this[ 'on' + event.type ]; if ( method ) { method.call( this, event ); } }; Dragger.prototype.onmousedown = Dragger.prototype.onpointerdown = function( event ) { this.pointerDown( event, event ); }; Dragger.prototype.ontouchstart = function( event ) { this.pointerDown( event, event.changedTouches[0] ); }; Dragger.prototype.pointerDown = function( event, pointer ) { event.preventDefault(); this.dragStartX = pointer.pageX; this.dragStartY = pointer.pageY; window.addEventListener( moveEvent, this ); window.addEventListener( upEvent, this ); this.onPointerDown( pointer ); }; Dragger.prototype.ontouchmove = function( event ) { // HACK, moved touch may not be first this.pointerMove( event, event.changedTouches[0] ); }; Dragger.prototype.onmousemove = Dragger.prototype.onpointermove = function( event ) { this.pointerMove( event, event ); }; Dragger.prototype.pointerMove = function( event, pointer ) { event.preventDefault(); var moveX = pointer.pageX - this.dragStartX; var moveY = pointer.pageY - this.dragStartY; this.onPointerMove( pointer, moveX, moveY ); }; Dragger.prototype.onmouseup = Dragger.prototype.onpointerup = Dragger.prototype.ontouchend = Dragger.prototype.pointerUp = function( event ) { window.removeEventListener( moveEvent, this ); window.removeEventListener( upEvent, this ); this.onPointerUp( event ); }; function BokehShape( options ) { this.create( options ); this.bokehSize = options.bokehSize || 5; this.bokehLimit = options.bokehLimit || 64; } BokehShape.prototype = Object.create( Shape.prototype ); BokehShape.prototype.updateBokeh = function() { // bokeh 0 -> 1 this.bokeh = Math.abs( this.sortValue ) / this.bokehLimit; this.bokeh = Math.max( 0, Math.min( 1, this.bokeh ) ); return this.bokeh; }; BokehShape.prototype.getBokehLineWidth = function() { return this.li.........完整代码请登录后点击上方下载按钮下载查看
网友评论0