1 /** 2 Copyright: Copyright (c) 2017-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.graphics.animation.spritesheetanimation; 7 8 import voxelman.math; 9 import voxelman.graphics; 10 11 struct AnimationFrame 12 { 13 Sprite sprite; 14 float timelineStart; 15 float timelineEnd; 16 } 17 18 struct SpriteSheetAnimation 19 { 20 AnimationFrame[] frames; 21 float totalDuration; 22 } 23 24 alias SpriteSheetAnimationRef = SpriteSheetAnimation*; 25 26 27 import std.json; 28 SpriteSheetAnimationRef loadSpriteSheetAnimation(string filename, TextureAtlas texAtlas) 29 { 30 // create filenames 31 import std.path : setExtension; 32 string imageFilename = setExtension(filename, "png"); 33 string metaFilename = setExtension(filename, "json"); 34 35 // load texture 36 import dlib.image.io.png; 37 auto image = new Bitmap(loadPNG(imageFilename)); 38 39 // parse frames 40 import std.file; 41 string metaFileData = cast(string)read(metaFilename); 42 auto jsonValue = parseJSON(metaFileData); 43 auto frames = parseFrames(jsonValue["frames"].array); 44 float totalDuration = frames[$-1].timelineEnd; 45 46 putFramesIntoAtlas(image, frames, texAtlas); 47 48 return new SpriteSheetAnimation(frames, totalDuration); 49 } 50 51 AnimationFrame[] parseFrames(JSONValue[] framesArray) 52 { 53 AnimationFrame[] frames; 54 float timelineStart = 0; 55 float timelineEnd = 0; 56 foreach(frameJson; framesArray) 57 { 58 auto frame = AnimationFrame( 59 Sprite(irect( 60 cast(int)frameJson["frame"]["x"].integer, 61 cast(int)frameJson["frame"]["y"].integer, 62 cast(int)frameJson["frame"]["w"].integer, 63 cast(int)frameJson["frame"]["h"].integer))); 64 auto durationMsecs = cast(int)frameJson["duration"].integer; 65 frame.timelineStart = timelineStart; 66 timelineEnd = timelineStart + durationMsecs / 1000.0; 67 frame.timelineEnd = timelineEnd; 68 timelineStart = timelineEnd; 69 frames ~= frame; 70 } 71 72 return frames; 73 } 74 75 // After this frames refer to positions in atlas 76 void putFramesIntoAtlas(Bitmap sourceBitmap, AnimationFrame[] frames, TextureAtlas texAtlas) 77 { 78 foreach(ref frame; frames) 79 { 80 ivec2 position = texAtlas.insert(sourceBitmap, frame.sprite.atlasRect); 81 frame.sprite.atlasRect.position = position; 82 } 83 } 84 85 enum AnimationStatus 86 { 87 playing, 88 paused 89 } 90 91 struct AnimationInstance 92 { 93 SpriteSheetAnimationRef sheet; 94 AnimationStatus status; 95 double timer = 0; 96 size_t currentFrame = 0; 97 vec2 scale = vec2(1, 1); 98 vec2 origin = vec2(0, 0); 99 void delegate() onLoop; 100 101 void update(double dt) 102 { 103 if (status != AnimationStatus.playing) return; 104 105 timer = timer + dt; 106 107 double loops = floor(timer / sheet.totalDuration); 108 if (loops != 0) 109 { 110 timer = timer - sheet.totalDuration * loops; 111 if (onLoop) onLoop(); 112 } 113 114 currentFrame = seekFrameIndex(timer); 115 } 116 117 Sprite currentFrameSprite() 118 { 119 return sheet.frames[currentFrame].sprite; 120 } 121 122 void pause() 123 { 124 status = AnimationStatus.paused; 125 } 126 127 void resume() 128 { 129 status = AnimationStatus.playing; 130 } 131 132 void gotoFrame(int frame) 133 { 134 currentFrame = clamp(frame, 0, sheet.frames.length-1); 135 timer = sheet.frames[currentFrame].timelineStart; 136 } 137 138 size_t seekFrameIndex(float timer) 139 { 140 if (sheet.frames.length < 2) return 0; 141 142 int right = cast(int)(sheet.frames.length-1); 143 int left = 0; 144 int middle = 0; 145 146 while (left <= right) 147 { 148 middle = (left + right) / 2; 149 if (timer > sheet.frames[middle].timelineEnd) left = middle + 1; 150 else if (timer <= sheet.frames[middle].timelineStart) right = middle - 1; 151 else return middle; 152 } 153 154 return middle; 155 } 156 }